<!--
{
  "documentType" : "article",
  "framework" : "FoundationModels",
  "identifier" : "/documentation/FoundationModels/composing-dynamic-sessions-with-instructions-and-profiles",
  "metadataVersion" : "0.1.0",
  "role" : "article",
  "title" : "Composing dynamic sessions with instructions and profiles"
}
-->

# Composing dynamic sessions with instructions and profiles

Adapt sessions dynamically at runtime by loading instructions and tools based
on the state of your app.

## Discussion

As you build features for sophisticated use cases, effective context management
becomes essential. For example, a cooking app that provides a guided multi-step
workflow where a person discovers a recipe, substitutes ingredients, searches
inventory, and provides step-by-step cooking guidance. All of these steps together
would be difficult to build well with a static session.

By default, a language model session evaluates instructions upon initialization,
and they remain static for the session. The dynamic profiles API allows you to
build your app so a session uses only what’s necessary based on the state of your
app. When the context of your app changes, the instructions, tools, and model
configuration change with it.

The framework introduces three composable layers for building dynamic apps that
interact with language models:

- **Dynamic instructions**: Re-evaluate before every model request and supply
  the instructions and tools needed at that time.
- **Profiles**: Associate dynamic instructions with session-level properties,
  like which model to use and the reasoning level.
- **Dynamic profiles**: Orchestrates transitions between multiple profiles
  and maintains one active profile at a time.

Because the body of dynamic instructions re-evaluates before each call to the
model, the model always sees a snapshot of your app’s current state.

## Declare dynamic instructions

You use [`DynamicInstructions`](/documentation/FoundationModels/DynamicInstructions) to declaratively specify the instructions and
list of tools that are necessary for the state your app is in. Conform a type to
[`DynamicInstructions`](/documentation/FoundationModels/DynamicInstructions) and implement its `body` property. Carefully consider
using only the instructions and tools that are necessary for the task since a
dynamic instruction evaluates before each model request.

In the `body` of your type, include any [`Instructions`](/documentation/FoundationModels/Instructions) block, [`Tool`](/documentation/FoundationModels/Tool) instances,
and nested [`DynamicInstructions`](/documentation/FoundationModels/DynamicInstructions). Because the `body` of your type evaluates
before each model request, add any necessary conditionals to load or exclude items
based on your runtime state.

```swift
struct PresentationInstructions: DynamicInstructions {
    // The data source for conditional instructions.
    var isEditingImage = true
    var isEditingAnimation = false

    var body: some DynamicInstructions {
        // The instructions and tools that remain the same across any use of this type.
        Instructions {
            "Help people improve their presentation."
        }
        ListPhotosTool()
        AddPhotoTool()

        // Depending on the state of the app, include additional instructions
        // that provide the model with more task-specific instructions and tools.
        if isEditingImage {
            ImageEditingInstructions()
        }
        
        if isEditingAnimation {
            AnimationEditingInstructions()
        }
    }
}
```

Because the `body` re-evaluates before every model request, switching from image
editing to animation editing automatically updates the active toolset.

Nest [`DynamicInstructions`](/documentation/FoundationModels/DynamicInstructions) conformances to keep each concern self-contained.
The custom `ImageEditingInstructions` type above, for example, bundles its own
instructions and tools for use across different parts of the app.

After declaring your [`DynamicInstructions`](/documentation/FoundationModels/DynamicInstructions), pass them to [`LanguageModelSession`](/documentation/FoundationModels/LanguageModelSession)
using the [`init(model:dynamicInstructions:history:)`](/documentation/FoundationModels/LanguageModelSession/init(model:dynamicInstructions:history:)) initializer:

```swift
let session = LanguageModelSession(
    dynamicInstructions: PresentationInstructions()
)
```

> Important: When conditionally providing instructions and tools, append them
> in place to improve latency from the use of model caching. For more
> information about prompt caching, see <doc://com.apple.foundationmodels/documentation/FoundationModels/optimizing-key-value-caching-in-language-model-sessions>.

## Configure a profile

A [`LanguageModelSession.Profile`](/documentation/FoundationModels/LanguageModelSession/Profile) associates [`DynamicInstructions`](/documentation/FoundationModels/DynamicInstructions) with
session-level properties, making instructions, tools, and modifiers more dynamic.
Compose a [`LanguageModelSession.Profile`](/documentation/FoundationModels/LanguageModelSession/Profile) based on the task you want to accomplish.
Parameterizing instructions, tools, and modifiers makes the API surface flexible
enough to fit your needs. For example, you might choose to parameterize the
model temperature and reasoning level depending on the state of your app:

```swift
Profile {
    // Custom instructions and tools for a creative task. 
}
// Use an instance of the PCC model you create in the parent profile.
.model(pccModel)
// Use a higher creative temperature value when a person likes poetry.
.temperature(likesPoetry ? 0.8 : 0.1)
// Perform deeper reasoning when a person likes astronomy.
.reasoningLevel(likesAstronomy ? .deep : .light)
```

## Orchestrate multiple profiles with a dynamic profile

Instead of writing an all-encompassing profile, break up the problem into
multiple domain-specific profiles. A [`LanguageModelSession.DynamicProfile`](/documentation/FoundationModels/LanguageModelSession/DynamicProfile)
orchestrates the transitions between your profiles by using conditionals and nesting.
When the active profile changes, the session adopts the new model and generation
options on the next model request. The following diagram illustrates a dynamic
profile managing three profiles, each with their own model configuration and
conditional instructions:

![A diagram that illustrates a dynamic profile that manages three profiles. There](images/com.apple.foundationmodels/composing-dynamic-sessions-with-instructions-and-profiles-dynamic-profile@2x.png)

A [`LanguageModelSession.DynamicProfileBuilder`](/documentation/FoundationModels/LanguageModelSession/DynamicProfileBuilder) enforces a hard constraint at
compile time so exactly one [`LanguageModelSession.Profile`](/documentation/FoundationModels/LanguageModelSession/Profile) is active at a time.
Instead of using parallel `if` blocks, use expressions so the compiler verifies the constraint:

```swift
struct PresentationProfile: LanguageModelSession.DynamicProfile {
    // Create an instance to the server model.
    var pccModel = PrivateCloudComputeLanguageModel()
    
    // The data source for the profile.
    var isEditingImage = true
    var isEditingAnimation = false

    // Determine which profile to load based on the current state.
    var body: some LanguageModelSession.DynamicProfile {
        if isEditingImage {
            Profile {
                ImageEditingInstructions()
            }
        } else if isEditingAnimation {
            Profile {
                AnimationEditingInstructions()
            }
            .model(pccModel)
            .temperature(0.2)
            .reasoningLevel(.light)
        } else {
            Profile {
                PresentationDynamicInstructions()
            }
            .temperature(0.8)
        }
    }
}
```

Pass the dynamic profile to [`LanguageModelSession`](/documentation/FoundationModels/LanguageModelSession) using the
[`init(profile:history:)`](/documentation/FoundationModels/LanguageModelSession/init(profile:history:)) initializer:

```swift
let session = LanguageModelSession(
    profile: PresentationProfile()
)
```

## Apply modifier precedence rules

Dynamic profiles and subprofiles support the same value modifiers, like
temperature, reasoning level, and so on. When the same modifier appears at multiple
levels, a three-tier precedence rule determines which value to use — from highest
to lowest priority:

1. **Call-site arguments** — Generation options you pass directly to `respond(to:options:)`
   override all profile and dynamic profile modifiers.
2. **Innermost dynamic profile or profile modifier** — The modifier closest to the
   subprofile declaration overrides a dynamic profile.
3. **Dynamic profile modifiers** — Act as defaults that apply to all subprofiles
   unless the modifier is overridden by a subprofile.

Use profile modifiers to establish baseline defaults, then let individual
subprofiles refine or override them:

```swift
// A top-level dynamic profile that includes a single subprofile.
struct WritingProfile: LanguageModelSession.DynamicProfile {
    var body: some LanguageModelSession.DynamicProfile {
        // By default, the temperature value applies to both branches in 
        // `WritingContent` unless a branch adds a temperature modifier.
        WritingContent()
            .temperature(0.5)
    }
}

// A dynamic profile that contains two states.
struct WritingContent: LanguageModelSession.DynamicProfile {
    // A custom writing mode that determines which subprofile to use.
    var mode: MyCustomWritingMode = .creative

    var body: some LanguageModelSession.DynamicProfile {
        switch mode {
        case .creative:
            // Use the temperature `1.0` because the profile-level modifier takes priority.
            Profile {
                CreativeWritingInstructions()
            }
            .temperature(1.0)  
        case .technical:
            // Inherit the temperature `0.5` from `WritingProfile`.
            Profile {
                TechnicalWritingInstructions()
            }
        }
    }
}
```

## Respond to life cycle events

Observe and react to key moments during a session by using life cycle modifiers.
Unlike value modifiers, life cycle callbacks accumulate across nested profiles.
When a profile and a subprofile both register a callback, the framework calls
both. The following shows the available modifiers and when they run:

|Modifier                                                                                                                      |Description                                                                               |
|------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
|``doc://com.apple.foundationmodels/documentation/FoundationModels/LanguageModelSession/DynamicProfile/onActivate(perform:)``  |Runs when the profile becomes active and allows for set up work.                          |
|``doc://com.apple.foundationmodels/documentation/FoundationModels/LanguageModelSession/DynamicProfile/onDeactivate(perform:)``|Runs when the profile becomes inactive and allows for teardown work.                      |
|``doc://com.apple.foundationmodels/documentation/FoundationModels/LanguageModelSession/DynamicProfile/onPrompt(perform:)``    |Runs after the user prompt appends to the transcript, but before the model request starts.|
|``doc://com.apple.foundationmodels/documentation/FoundationModels/LanguageModelSession/DynamicProfile/onResponse(perform:)``  |Runs after the model produces a response.                                                 |
|``doc://com.apple.foundationmodels/documentation/FoundationModels/LanguageModelSession/DynamicProfile/onToolCall(perform:)``  |Runs when the model invokes a tool.                                                       |
|``doc://com.apple.foundationmodels/documentation/FoundationModels/LanguageModelSession/DynamicProfile/onToolOutput(perform:)``|Runs when a tool call produces output.                                                    |

Use life cycle events as validation checkpoints by throwing an error. Throwing an
error inside a life cycle callback propagates to the caller’s
[`respond(to:options:)`](/documentation/FoundationModels/LanguageModelSession/respond(to:options:)) or
[`streamResponse(to:options:)`](/documentation/FoundationModels/LanguageModelSession/streamResponse(to:options:)) call, letting you raise
errors that surface directly to your call site:

```swift
Profile {
    MyCustomFileAccessInstructions()
    MyCustomReadFileTool()
}
.onToolCall { toolCall in
    // Runs before the framework invokes the tool and allows for checking 
    // whether the app is in a state to run the tool.
    guard myAccessPolicy.permits(toolCall) else {
        throw MyAccessPolicyError.denied(toolCall.toolName)
    }
}
.onToolOutput { toolCall, output in
    // Runs after the tool. This is a good place to log any necessary activity.
}
```

## Access session-level properties

The building blocks of dynamic instructions and profiles help you define what
the model sees and how it’s configured. Use `@SessionProperty` to access session
properties from within a [`LanguageModelSession.DynamicProfile`](/documentation/FoundationModels/LanguageModelSession/DynamicProfile),
[`LanguageModelSession.Profile`](/documentation/FoundationModels/LanguageModelSession/Profile), [`DynamicInstructions`](/documentation/FoundationModels/DynamicInstructions), and [`Tool`](/documentation/FoundationModels/Tool).

The session history provides the transcript entries after instructions such as
prompts, responses, tool calls, and tool outputs. The history excludes instructions
segments from `DynamicInstructions`. Accessing the history shared property gives
you the ability to perform updates like compaction and summarization. Use
`@SessionProperty` to access and modify transcript entries:

```swift
// Get a reference to the session history.
@SessionProperty(\.history) 
var history

var body: some LanguageModelSession.DynamicProfile {
    Profile {
        Instructions("You are a helpful assistant.")
        TodoWriteTool()
    }
    .onResponse { 
        // When the entries exceed `100`, perform a stateful update to the 
        // history so it only includes the last `50` entries.
        if history.count > 100 {
            history = history.suffix(50)
        }
    }
}
```

> Note: Because model output influences the evaluation of ``doc://com.apple.foundationmodels/documentation/FoundationModels/DynamicInstructions``
> and ``doc://com.apple.foundationmodels/documentation/FoundationModels/Tool``, the session history is read-only in these contexts.

When you need session-scoped properties beyond the built-in history, apply the
[`SessionPropertyEntry()`](/documentation/FoundationModels/SessionPropertyEntry()) macro to a stored property in an extension on
[`SessionPropertyValues`](/documentation/FoundationModels/SessionPropertyValues). This makes the property accessible from the components
within the session when you use [`LanguageModelSession.SessionProperty`](/documentation/FoundationModels/LanguageModelSession/SessionProperty). For example, to create a
custom [`Tool`](/documentation/FoundationModels/Tool) to access shared state, extend [`SessionPropertyValues`](/documentation/FoundationModels/SessionPropertyValues) with a
new property and then access it with [`LanguageModelSession.SessionProperty`](/documentation/FoundationModels/LanguageModelSession/SessionProperty) from the tool:

```swift
extension SessionPropertyValues {
    @SessionPropertyEntry 
    var activatedSkills: [String: Bool] = [:]
}

struct PlannerTool: Tool {
    let description = "Update the state of the activated skills"
    
    // Read the shared session state for the currently activated skills.
    @SessionProperty(\.activatedSkills) 
    var activatedSkills

    @Generable
    struct Arguments {
        @Guide(description: "The skills to activate")
        var skills: [String]
    }

    func call(arguments: Arguments) -> String {
        // When the model calls this tool, update the skills to an active state.
        for skill in arguments.skills {
            activatedSkills[skill] = true
        }
        return "Activated: \(arguments.skills.joined(separator: ", "))"
    }
}
```

## Create reusable components with a custom modifier

A [`LanguageModelSession.DynamicProfileModifier`](/documentation/FoundationModels/LanguageModelSession/DynamicProfileModifier) allows you to compose traits
like model selection, temperature, life cycle callbacks, and so on, into a
self-contained modifier that you attach to your profile. A modifier receives the
wrapped content and returns a new [`LanguageModelSession.DynamicProfile`](/documentation/FoundationModels/LanguageModelSession/DynamicProfile) `body`.
For example, the following creates a low temperature modifier for debugging that
logs debug information after a request:

```swift
struct DebugProfileModifier: LanguageModelSession.DynamicProfileModifier {
    func body(content: Content) -> some LanguageModelSession.DynamicProfile {
        content
            .temperature(0.0)
            .onResponse { response in
                print("Debug response: \(response)")
            }
    }
}

extension LanguageModelSession.DynamicProfile {
    func debug() -> some LanguageModelSession.DynamicProfile {
        self.modifier(DebugProfileModifier())
    }
}
```

To use a custom modifier, apply it to your [`LanguageModelSession.Profile`](/documentation/FoundationModels/LanguageModelSession/Profile):

```swift
Profile {
    Instructions("You are a helpful assistant.")
}
.debug()
```

## Transform the session history

Because the transcript grows with every turn in a session, context
size limits become important to consider. Before prompting the model, a profile
can transform the transcript it sends to filter entries that aren’t necessary for
the request. For example, you might drop completed tool calls, or even remove sensitive
information, before sending a request to a model. Use the
[`historyTransform(_:)`](/documentation/FoundationModels/LanguageModelSession/DynamicProfile/historyTransform(_:)) modifier to receive the
current history before each model request and return only the entries to use for
the request. The following only sends the last twenty entries to the model:

```swift
Profile {
    Instructions("You help people generate fun and interesting book ideas.")
    MyCustomBookTool()
}
.historyTransform { history in
    // Perform a local transformation before prompting the model. This transform
    // doesn't affect the global state of the transcript, so you're not losing
    // existing transcript context.
    Array(history.suffix(20))
}
```

When a [`LanguageModelSession.DynamicProfile`](/documentation/FoundationModels/LanguageModelSession/DynamicProfile) coordinates multiple profiles,
[`historyTransform(_:)`](/documentation/FoundationModels/LanguageModelSession/DynamicProfile/historyTransform(_:)) allows each profile to
manage its own view of the history. One profile compresses the history
for a small on-device model, and a profile that uses a server model — with a
much larger context size — gets the full history.

---

Copyright &copy; 2026 Apple Inc. All rights reserved. | [Terms of Use](https://www.apple.com/legal/internet-services/terms/site.html) | [Privacy Policy](https://www.apple.com/privacy/privacy-policy)