Build Agentic Swift Apps With Dynamic Profiles
The first year of the Foundation Models framework was mostly about getting a local model into app code: prompts, streaming, structured output, and tool calling.
WWDC26’s Dynamic Profiles are about what happens after that first demo works.
The code below follows Apple’s WWDC26 examples for the SDKs that include LanguageModelSession.DynamicProfile.
A real AI feature rarely has one fixed mode. It might start by extracting information from an image, then call a tool to save structured data, then switch to a bigger reasoning model to brainstorm, then return to a smaller on-device model for a quick follow-up.
You can build that manually by tearing down and recreating sessions, copying transcripts around, changing tool arrays, and hoping you did not lose important context. Dynamic Profiles give that pattern a declarative home.
The problem: sessions become orchestration code
Imagine a craft journal app. The user adds a photo of an origami project. The assistant should:
- Analyze the image and record useful facts.
- Switch into brainstorming mode.
- Suggest future projects using a stronger reasoning model.
- Keep the conversation history intact.
Without Dynamic Profiles, the orchestration code wants to look like this:
import FoundationModels
import Observation
@Observable
final class CraftAssistantState {
var mode: AssistantMode = .analysis
}
@Generable
enum AssistantMode: String, Codable, Sendable {
case analysis
case brainstorm
}
final class CraftAssistant {
let state: CraftAssistantState
var session: LanguageModelSession?
init(state: CraftAssistantState) {
self.state = state
}
func updateSession() {
let transcript = session?.transcript.dropFirstInstructions() ?? Transcript()
switch state.mode {
case .analysis:
session = LanguageModelSession(
tools: [
RecordImageAnalysisTool(),
SwitchModeTool(state: state)
],
instructions: "Analyze the user's craft project and record what you find.",
transcript: transcript
)
case .brainstorm:
session = LanguageModelSession(
tools: [
RecordBrainstormTool()
],
instructions: "Brainstorm practical craft ideas from the recorded analysis.",
transcript: transcript
)
}
}
}
That is manageable with two modes. It gets messy when the feature grows. You have to decide when to rebuild the session, which parts of the transcript to preserve, which instructions to drop, and which tools should be visible.
The model also needs a way to switch modes. A small tool can do it:
import FoundationModels
struct SwitchModeTool: Tool {
let name = "switchMode"
let description = "Switch the assistant to a different mode."
let state: CraftAssistantState
@Generable
struct Arguments {
let mode: AssistantMode
}
func call(arguments: Arguments) async throws -> some PromptRepresentable {
state.mode = arguments.mode
return "Switched to \(arguments.mode)."
}
}
That works, but the profile of the session is still being managed imperatively.
The Dynamic Profile version
A Dynamic Profile describes what should be in the session right now. The profile can depend on observable app state, and the framework handles the transition when that state changes.
The basic shape from Apple’s WWDC26 session looks like this:
import FoundationModels
struct CraftProfile: LanguageModelSession.DynamicProfile {
var body: some LanguageModelSession.DynamicProfile {
Profile {
Instructions {
"""
You are an expert crafting assistant.
Record craft project image analyses using the recordImageAnalysis tool.
"""
}
RecordImageAnalysisTool()
}
}
}
let session = LanguageModelSession(profile: CraftProfile())
That is the simplest version: one active profile with instructions and tools.
The interesting version switches on app state:
import FoundationModels
struct CraftProfile: LanguageModelSession.DynamicProfile {
let state: CraftAssistantState
let privateCloudCompute: PrivateCloudComputeLanguageModel
var body: some LanguageModelSession.DynamicProfile {
switch state.mode {
case .analysis:
Profile {
Instructions {
"""
Analyze the user's craft photos.
Record concrete materials, colors, and techniques.
"""
}
RecordImageAnalysisTool()
SwitchModeTool(state: state)
}
case .brainstorm:
Profile {
Instructions {
"""
Suggest a short list of craft projects based on the recorded analysis.
Prefer projects the user can start with similar materials.
"""
}
RecordBrainstormTool()
}
.model(privateCloudCompute)
.reasoningLevel(.deep)
}
}
}
Then create the session once:
let state = CraftAssistantState()
let profile = CraftProfile(
state: state,
privateCloudCompute: PrivateCloudComputeLanguageModel()
)
let session = LanguageModelSession(profile: profile)
This is the part that feels right for SwiftUI. App state controls the active profile. The profile controls the current instructions, tools, model, and reasoning level. The session remains one conversational surface.
A profile resolves to one active profile
The most important sentence in Apple’s explanation is that a Dynamic Profile resolves to a single active Profile at any given time.
That means you should not think of this as “append all possible tools and hope the model picks the right one.” You are narrowing the context to the mode the feature is in right now.
That has practical benefits:
- The model sees fewer irrelevant tools.
- Instructions can be direct instead of full of conditionals.
- You can use a small on-device model for simple work.
- You can switch to Private Cloud Compute only for the part that benefits from deeper reasoning.
- The transcript can keep continuity across mode changes.
That last point is what makes this feel like an agentic app primitive instead of just a nicer initializer.
Where I would use it
Dynamic Profiles are a good fit when the same user task has phases.
A health app might use one profile to extract workout notes, another to ask clarifying questions, and another to generate a plan.
A photo app might use one profile with Vision tools for image understanding, then another with a search tool for finding related memories.
A writing app might use one profile for local grammar cleanup, another for outlining with a larger reasoning model, and another for final formatting.
I would not use Dynamic Profiles for every Foundation Models feature. If a feature has one prompt, one tool set, and one model, a normal LanguageModelSession is still simpler.
The point is to reach for profiles when you catch yourself writing session-orchestration code.
Be explicit about privacy, cost, and capability
Dynamic Profiles make model switching easy, which means you need to be more careful about when switching is appropriate.
Apple’s WWDC26 Foundation Models session calls out a few model choices:
SystemLanguageModelfor the on-device model.PrivateCloudComputeLanguageModelfor Apple’s larger private cloud model.CoreAILanguageModelandMLXLanguageModelfor local custom models.- Partner model packages from providers such as Anthropic and Google.
Those are not interchangeable from a product perspective.
Private Cloud Compute has a different availability and entitlement story than the on-device model. Apple also calls out daily user limits: iCloud+ subscribers get higher limits, and developers in the App Store Small Business Program with fewer than 2 million first-time App Store downloads can use PCC with no cloud API cost, subject to entitlement and eligibility requirements. Third-party server models can involve authentication, billing, and API keys that should never live in an app binary. Local custom models have storage, memory, and first-run preparation costs.
So I would make the model switch visible in code and boring in UX:
enum AssistantCapability {
case localOnly
case privateCloudReasoning
case thirdPartyServer
}
struct AssistantConfiguration {
var capability: AssistantCapability
var allowsCloudReasoning: Bool
var requiresEntitlement: Bool
var userFacingExplanation: String
}
The exact product UI depends on the app, but the code should not hide that a brainstorm profile is using a different model class than an image-analysis profile.
Keep tools mode-specific
The other big win is tool scoping.
In the analysis profile, a RecordImageAnalysisTool makes sense. In brainstorm mode, it is probably noise. In a purchasing flow, a tool that spends money should not be visible while the model is summarizing notes.
That principle matters even more as your app gets more capable. Tool calling is powerful because the model can act. Dynamic Profiles help you decide which actions exist at each step.
My rule of thumb:
- Give the model only the tools that are useful in the current mode.
- Put destructive or expensive tools behind explicit user confirmation.
- Treat mode switches as app state transitions, not just prompt text.
- Add evaluations for the transitions, not only the final answer.
Why this is worth writing around
Dynamic Profiles are exciting because they are not just another model feature. They are an app architecture feature.
They let a Swift app describe a changing AI context the same way SwiftUI describes a changing UI context: state changes, and the declarative description resolves to the current shape.
That does not make agentic apps easy. You still need good tools, good state, good permission boundaries, and a way to evaluate the results. But it removes a pile of orchestration glue from the middle of the feature.
That is usually where the best framework APIs live: not replacing your product judgment, just clearing enough brush that you can actually use it.
Further reading: