Blog • Foundation Models
Apple Intelligence in 2026: The JSON Exchange Between Foundation Models and App Intents
In 2026 the same app has two entrances. An on-device LanguageModelSession decodes a typed value, and the system calls perform() on an App Intent. What you persist is neither of those values as-is. It is an exchange envelope you own.
The product story is in why the iPhone 18 is more than hardware. Whether Siri becomes an agent is in Will Siri AI become an AI agent. This article is only the envelope: which fields you persist, and when you must not run the action.
- content is the object the model decoded
- parameters is the object you pass to perform()
- When complete is false, parameters stays null
- When schemaVersion changes, an old envelope cannot be replayed as-is
Hold this first: The framework turns a Generable type into a schema and decodes a value. That stays inside the model session. The envelope is what you store and reconcile before deciding to run the action. Next: two contracts, the fields, one JSON document, then four reasons to stop.
Two contracts, two calls
How the on-device model fills a typed value is documented under Generable. The type becomes a schema, the model fills it, and the framework decodes a value. A static type uses @Generable. Without one, the session can return GeneratedContent for a GenerationSchema. Both land in the envelope as content.
An App Intent is a different call path. Parameters live on @Parameter. See Adding parameters to an App Intent. When the system calls perform(), there is no LanguageModelSession and no Generable content.
- 1The session produces content
respond(to:generating:) returns a Response. Until generation finishes, the value is PartiallyGenerated and fields may still be empty. There is no parameter object you can run yet.
- 2Project the parameters
Copy into parameters only the fields the Intent accepts. A rationale the model added stays in content. The objects may share names. They are not the same object.
- 3Write the envelope before you run
Persist source, sessionId, schemaVersion, complete, content, intent, and parameters as one JSON document. If complete is not true, do not call perform().
Fields in the envelope
These keys belong to your envelope. They are not fields in Apple's decoded value.
| Field | Who writes it | Role |
|---|---|---|
source | Your app | foundation-models or app-intent. It names which entrance produced the value. |
sessionId | Your app | Only a model session has one, for example lm_01. A system call stores null. |
schemaVersion | Your app | The generation of your Generable field list. This example uses 3. Bump it when fields change. Do not replay an old envelope into the new Intent. |
generable | Your app | The type name, for example OrderDraft. A direct system call stores null. |
complete | You copy it from the generation state | false means generation is still running and fields can still change. Only true allows you to project and consider running. |
content | The decoded model value | The decoded object. It may contain extra fields, such as rationale. A system call stores null. |
intent | Your app | The App Intent you will call, or that the system is calling, for example PlaceOrderIntent. |
parameters | Your projection, or the system arguments | The object passed to perform(). While incomplete, store null. On a system call, these are the arguments the system already supplied. |
content and parameters may use the same field names. They are still two objects. If you pass content straight to perform(), a rationale the model added enters the action.
One JSON document you can store
The envelope and schema below are your contract, not Apple's wire format. JSON Schema in JSONNote checks types, enums, and required keys. It cannot express that you may run only when complete is true. That decision stays in the app.
{
"source": "foundation-models",
"sessionId": "lm_01",
"schemaVersion": 3,
"generable": "OrderDraft",
"complete": true,
"content": {
"itemId": "sku_42",
"quantity": 1,
"fulfillment": "pickup",
"rationale": "User asked for pickup."
},
"intent": "PlaceOrderIntent",
"parameters": {
"itemId": "sku_42",
"quantity": 1,
"fulfillment": "pickup"
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["source", "sessionId", "schemaVersion", "generable", "complete", "content", "intent", "parameters"],
"properties": {
"source": { "enum": ["foundation-models", "app-intent"] },
"sessionId": { "type": ["string", "null"] },
"schemaVersion": { "type": "integer" },
"generable": { "type": ["string", "null"] },
"complete": { "type": "boolean" },
"content": { "type": ["object", "null"] },
"intent": { "type": "string" },
"parameters": { "type": ["object", "null"] }
}
}
rationale stays only in content. The type map for action parameters is in App action fields and JSON Schema. This article does not repeat that table.
Do not perform while incomplete
{
"source": "foundation-models",
"sessionId": "lm_01",
"schemaVersion": 3,
"generable": "OrderDraft",
"complete": false,
"content": {
"itemId": "sku_42"
},
"intent": "PlaceOrderIntent",
"parameters": null
}
This envelope passes the schema above, because parameters may be null. A valid shape is still not a reason to run. PartiallyGenerated means later tokens can still change fields that already appeared. Wait until complete is true, then project parameters from the full content. If you put an object in parameters while generation is unfinished, the schema still accepts it.
| Envelope | Why you stop |
|---|---|
| complete is false | Later tokens can still change fields that already appeared. |
| schemaVersion is not the current field list | The old content does not match the Intent you have now. |
| On foundation-models, parameters has a key that content lacks | The projection must not invent a value the model never wrote. |
| On app-intent, content is not null | System arguments are not model output. Do not copy them into content and pretend the model generated them. |
Use a JSON diff to see an extra key. That check applies only when source is foundation-models. On a system call, content is null, so you cannot require every parameters key to have appeared in content.
How Tool meets App Intent
A Foundation Models Tool is called by your session. See Expanding generation with tool calling. The Arguments received by call(arguments:) are Generable too. Write them into content first. Only after you project parameters is perform() in question.
When the system calls the Intent directly, set source to app-intent, and set sessionId, generable, and content to null. parameters are the values the system already passed. Do not invent a sessionId and disguise the call as model output.
| Foundation Models Tool | App Intent | |
|---|---|---|
| Who calls it | Your LanguageModelSession | Siri, Shortcuts, Spotlight |
| Input | Generable Arguments | Values on @Parameter |
| Method you implement | call(arguments:) | perform() |
| source in the envelope | foundation-models | app-intent |
How this differs from the other articles
| Product and Siri articles | The action-fields article | This article | |
|---|---|---|---|
| Answers | Why it is more than hardware, and whether Siri becomes an agent | Which fields may enter the action | Which JSON sits between the model value and the action parameters |
| Do not look here for | Another launch recap | The type map from AppEnum and entity id | Passing content unchanged as the input to perform() |
Check the envelope in JSONNote
- 1Pick the right operation
Open JSON Schema. Set Operation to Check with Schema. Paste the envelope in the first field and the schema above in the second. source must be foundation-models or app-intent. complete must be a boolean. parameters must be an object or null.
- 2Reject three bad documents
Set source to siri, set complete to the string true, or delete parameters. Each of those fails. The unfinished envelope, with complete false and parameters null, passes. Passing still does not mean you call perform().
- 3Diff the two objects
Open JSON Compare Diff. Paste content in the first field and parameters in the second. rationale should appear only in the first. A key that shows up only in the second was written by the projection, not by the model. Use this diff only when source is foundation-models.
Common questions
Can Generable output go straight to perform()?
No. The output is stored as content. perform() receives only the projected parameters. The envelope keeps both so you can reconcile them.
If the schema passes, can you run the action?
Not yet. This schema only checks types, enums, and required keys. complete false with parameters null is still a valid shape. Before you run, complete must be true, and on the model entrance parameters must not contain a key missing from content.
Are Tool and App Intent the same call?
No. A Tool is called by LanguageModelSession. An App Intent is called by Siri, Shortcuts, or Spotlight. The same task can exist on both sides. Both inputs land in this envelope first.
If schemaVersion changes, can you replay an old log?
Not as-is. The version marks the generation of your Generable field list. After the fields change, old content does not match the current Intent. Stop, write an explicit mapping, then decide whether to run.
Is this the same article as the one on action fields and JSON Schema?
No. That article aligns parameter types into a field list. This one is the envelope you store between the model value and that list: entrance, completion, version, and content kept apart from parameters.
Summary
Foundation Models and App Intents are two entrances. The exchange envelope puts entrance, completion, schema version, content, and parameters in one JSON document so you can stop before perform().
The decision: keep content in the envelope; perform() receives only parameters; do not run when complete is false; do not replay a mismatched schemaVersion; parameters must not contain a key that content does not have.
Next: let this exchange JSON pass the schema, then diff content against parameters.