[AVAILABLE] since 2026-09-21. Design:. Code:
packages/runtime/src/ExecutionIntentService.ts,
ExecutionIntentFinalizer.ts, packages/crypto/src/ExecutionIntentCrypto.ts.
Verified live on 2026-09-21, see What was verified.The problem this solves
An Execution Trust Record contains the result of the execution, so it can only be built after the action has been released to the connector. Before this feature, that left a window: if the record could not be produced or stored after release, an action had run and nothing signed described it. The runtime reported500 EXECUTION_RECORD_INCOMPLETE and logged the identifiers, but the context needed to rebuild the record existed only in the memory of the failed request.
An Execution Intent closes that window. Before the connector is called, the runtime signs and stores a record of what it is about to release. If that cannot be done, nothing is released.
What happens to a request, in order
1
Accept
The request is validated and recorded as received.
2
Decide and authorize
Policy is evaluated. If it approves, the runtime signs an execution
authorization. A policy refusal ends here with
403, and nothing is
released.3
Signing readiness check
The runtime proves the signing path works (
503 SIGNING_UNAVAILABLE if
not). Nothing is released.4
Sign and store the Execution Intent
The runtime signs the intent and writes it to the database. If this fails,
the caller gets
503 EXECUTION_INTENT_UNAVAILABLE and nothing is
released.5
Release
The action is released to the connector.
6
Save the execution context
Right after the connector answers, the execution context is saved on the
intent, so the Trust Record can be rebuilt later. This step is best effort:
if it fails, it is logged at critical severity and the request carries on.
7
Build and store the Trust Record
The signed Execution Trust Record is built and stored. If this fails, the
caller gets
500 EXECUTION_RECORD_INCOMPLETE.8
Mark the intent FINALIZED
The intent is marked
FINALIZED with the Trust Record id, and the saved
context is deleted. This step is best effort.9
Verify and issue the receipt
As before.
What is signed
The intent contains only facts that exist before release. It never contains the execution result, and never contains the raw intent parameters.
A real captured intent is in Get an Execution Intent.
What an intent proves, and what it does not
The honest reading of an intent with no Trust Record is: the action may or may not have run, and someone has to check the connector.
The five states
The state is operational status kept next to the signed intent. It is not signed, so it changes as the request progresses.FINALIZED also records finalizationMode: INLINE when the Trust Record was produced in the original request, REPAIRED when it was rebuilt afterwards by finalize.
RESOLVED also records resolution (NOT_EXECUTED or EXECUTED, what the operator found at the connector), resolutionNote, resolvedBy and resolvedAt. That is an attributed, timestamped statement by a person, stored in the unsigned status. It is not tamper evident, and it is not a signed Trust Record. For an action that ran, the signed evidence is still the intent, and the operator’s note is the trail.
Every failure, and what the caller sees
Find and repair a released action with no signed record
These routes need a credential provisioned as a verified human (credentialHolderType: USER). Any other credential gets 403 NON_HUMAN_CALLER_DENIED. Set two variables first.
Step 1. List what needs attention
{ "intents": [ { "intent": {...}, "status": {...} } ] }, oldest first, at most 50 by default (?limit= up to 200). Every entry is an action that may have been released with no signed record, or whose outcome nobody has reconciled yet. A RESOLVED intent no longer appears.
Step 2. Decide by state
- RELEASED
- PREPARED or ERRORED
- FINALIZED
Go to step 3. This is the case finalize exists for.
Step 3. Finalize
- It never calls the connector. It reads the execution context saved right after release and runs the same record building step the runtime uses.
- It also verifies the record and generates its receipt, so the repaired record ends in the same state as an ordinary one.
- It is idempotent. If a Trust Record already exists it returns it with
outcome: ALREADY_FINALIZEDand builds nothing. Running it twice, or twice at once, produces one record. - Success returns
200withoutcome: FINALIZEDand the Trust Record.
Step 4. Close an intent you reconciled at the connector (PREPARED or ERRORED)
Do this only after you have checked the connector, usingbusinessTransactionId, action and target from the intent. Then record what you found, and a note saying what you checked.
note is required, at most 2000 characters, and is the only record of what you found. resolvedBy is taken from your credential, and resolvedAt is set by the server.
Resolving never calls a connector.
Step 5. Confirm a repair
status.state should now be FINALIZED and status.finalizationMode should be REPAIRED. The rebuilt record is then returned by GET /trust-records/<businessTransactionId> like any other.
A repaired record has its own createdAt, the time of the repair. The times of the execution itself are inside the record’s execution evidence. Compare status.releasedAt with the record’s createdAt to see how long the gap was.
Verify an intent
- With the API (no credential)
- Offline (no network, no database)
POST /execution-intents/verify takes the intent itself and returns { "valid": true } or { "valid": false }. It needs no API key, reads no storage, and is documented at Verify an Execution Intent.Both SDKs have an offline verifier:
verify_execution_intent_offline in
Python, and verifyExecutionIntentOffline in TypeScript from 1.3.0 (published
2026-09-25). On TypeScript 1.2.0 and earlier, use the API route or the script
above. See Use the SDKs and SDK coverage of the
API.Use the SDKs
A non human credential gets an
AuthorizationError. Resolving an intent that cannot be closed gets a ConflictError. A missing intent gets a NotFoundError. 503 EXECUTION_INTENT_UNAVAILABLE arrives on execute as an InternalServerError whose server code is EXECUTION_INTENT_UNAVAILABLE, and nothing was executed.
intent field of GET /execution-intents/<id> as a plain dictionary and the public key as text, and needs no network and no database.
Turn it on: the deployment order matters
Execution Intents are enforced by default. In production, and wheneverNODE_ENV is not exactly test or development, there is no switch to turn them off. See EXECUTION_INTENTS_CHECK in the environment variable reference.
-
Apply
supabase/migrations/20260921120000_add_execution_intents.sqlto the production database. It only adds a table and is safe to run twice. -
Confirm the table exists.
The output must be
execution_intents. Empty output means the migration did not apply. - Deploy.
-
Check
GET /ready. It returnsREADY. If the table is missing it returns503withstatus: NOT_READYand areasonnaming the migration file, so a skipped migration is caught at the readiness check and not on the first real request. -
Check the startup log for
executionIntentsConfigured: true.
Cost
One more signing operation and one more database write happen before every release, and two more writes after it.Limits, stated plainly
- An intent proves what was about to be released. It does not prove release or the result.
- Finalize can only rebuild a record when the execution context was saved. When that save fails, finalize refuses with
409and the outcome must be established from the connector. - Transactions created before this feature have no intent. Their behavior is unchanged.
- Closing an intent (resolve) records an operator’s statement in unsigned status. It is not tamper evident and it does not create a Trust Record.
- The list route returns intents across all callers, so it needs a verified human credential.
- The SDK methods for the intent routes are available from SDK 1.2.0. Earlier versions do not have them. The TypeScript SDK has an offline intent verifier from 1.3.0, published 2026-09-25; 1.2.0 has none.