Recipes
A recipe is an immutable snapshot of a finished agent-session — its structured intent_log at the moment of capture. Recipes preserve a completed flow as a durable reference you can inspect without running the session again. A recipe does not keep a copy of the session’s transcript.
The current surface covers create, list, read, and delete:
POST /v1/recipes, GET /v1/recipes, GET /v1/recipes/{id}, and
DELETE /v1/recipes/{id}. There is no recipe-execution endpoint;
start a new agent-session to run another task.
Team workspaces. Every recipe route acts in the workspace you work
in: your own account, or the team owner’s that X-Driftstack-Account
names. In a team owner’s workspace every recipe route, reads included,
needs the admin role (403 otherwise), because a recipe is a saved
copy of an agent-session’s steps and the owner’s agent-sessions are read
by admins only. A recipe saved there belongs to the owner and counts
against the owner’s plan.
Resource shape
{
"id": "rec_<uuid>",
"account_id": "<account-uuid>",
"agent_session_id": "agt_<uuid> | null",
"label": "my checkout flow",
"description": "Snapshot of the example.com checkout session.",
"intent_count": 12,
"created_at": "<ISO-8601>",
"updated_at": "<ISO-8601>"
}
agent_session_id is null when the originating agent-session
has been deleted (the recipe survives when the session is deleted).
intent_count is the length of the
flattened intent_log. The list endpoint omits the intent array for
payload weight; fetch a single recipe with GET /v1/recipes/{id}
to get its public intent_log. Sensitive type steps retain their
selector, order, and sensitive: true marker but omit value. The
value is never stored. A step
that typed a saved credential or a session secret keeps its
{{credential:name}} placeholder (a name, not a value), and a card
number anywhere in a recipe is kept only as [card ending 1234].
The omission covers the value field of an interact intent, and
only that field: for any action marked sensitive: true, and for a
type step whose selector names a sensitive field even when it is not
marked. A sensitive selector redacts only type, and no other intent
kind is redacted here. This is the same
projection the agent-session transcript
stream and turn responses apply, and a different flag from the
sensitive on POST /v1/sessions/:id/interact
(sessions), which only shapes typing behaviour in
the session.
Create
POST /v1/recipes
Request body:
{
"agent_session_id": "agt_<uuid>",
"label": "my checkout flow",
"description": "Snapshot of the example.com checkout session."
}
agent_session_id— required. Must be a session of the workspace you save into: your own account’s, or, in a team owner’s workspace, the owner’s. The recipe is filed under that same account. A session of any other account returns 404.label— required. 1-120 characters after trim.description— optional. Up to 2000 characters.
Response 201 Created returns the resource above.
A recipe saved while its session is running keeps the steps the
running server still holds as they ran, typed values and full addresses
included. Any other step — every step once the session has ended, and a
running session’s earlier steps after the server restarted — comes from
what Driftstack keeps of a session: each step and the site it was on,
without typed values, with the words in selectors and text= locators cut
to …, and with each address cut to its site. When any step comes from
that copy the response says so in notice, and those steps may need to be
re-recorded before the recipe is replayed:
"Some steps of this recipe come from the stored copy of its session, …".
A session is saved once. A session already saved as a recipe cannot be
saved again under another label or description: that answers 409, with
the saved recipe’s id in recipe_id. Delete that recipe to save the
session again. Repeating the exact same save — same session, label and
description, as a retry does — returns the recipe already saved, 201,
without storing a second copy.
Recipes per account. Each plan keeps up to a number of recipes:
| Plan | Tier id | Recipes |
|---|---|---|
| Trial | trial |
10 |
| Starter | starter_v3 |
50 |
| Pro | pro_v3 |
200 |
| Team | team_v3 |
500 |
| Scale | scale_v3 |
1,000 |
| Enterprise | enterprise |
Custom |
Enterprise’s limit is set by contract.
The earlier plans, no longer offered, keep their limits for the accounts that hold them:
| Plan | Tier id | Recipes |
|---|---|---|
| Personal | solo_manual |
50 |
| Team | team_manual |
200 |
| Agency | agency_manual |
500 |
| API Starter | api_starter |
100 |
| API Builder | api_builder |
500 |
| API Scale | api_scale |
1,000 |
A save past the limit is refused 429 with the tier-limit problem
(“Your plan keeps up to 10 recipes, and this account has 10. Delete a
recipe to save a new one.”), carrying limit, current, resource: "recipe" and tier.
List
GET /v1/recipes
Lists the recipes of the workspace you act in, newest first. Cursor-paginated:
limit— optional. 1-100, defaults to 50.cursor— optional. Opaque cursor from a prior page’snext_cursor.
Response 200 OK:
{
"data": [],
"has_more": true,
"next_cursor": "<opaque> | null"
}
Each data entry is the resource shape above without the
intent_log array — list items carry only intent_count for
payload weight. Fetch a single recipe to get the public saved
steps. next_cursor is null on the last page.
Suggest a label/description
GET /v1/agent-sessions/{id}/recipe-suggestion
Before deciding whether to save a session as a recipe, fetch a
deterministic label + description suggestion derived from that
session’s own intent_log — the same data POST /v1/recipes would
capture. Read-only; safe to call speculatively (no recipe is created).
Response 200 OK:
{
"suggested_label": "Fill form on example.com",
"suggested_description": "Navigates to example.com, fills 2 fields, taps 1 element, submits.",
"intent_count": 5
}
The suggestion is a heuristic over the session’s own intents (distinct
navigate hostnames, interact-action counts) — not a cross-customer ML
model. It never inspects or trains on any other account’s data. A
session with no navigate/interact intents still returns a usable
generic suggestion rather than an error. id uses the same cross-account
404 contract as the rest of this surface (existence not leaked).
Get one
GET /v1/recipes/{id}
Returns a single recipe including its public intent_log array (the
list endpoint omits it). Sensitive type intents omit value, even
when sensitivity is inferred from a password, OTP, PIN, card, or API
key selector; these steps still carry sensitive: true so clients can
render them accurately. Other intent fields are unchanged. A
non-existent id — or one belonging to another account — returns 404;
the server doesn’t distinguish missing from forbidden, to avoid
leaking existence.
Delete
DELETE /v1/recipes/{id}
Deletes a recipe. Response 204 No Content. A non-existent id — or
one belonging to another account — returns 404, the same
anti-enumeration contract as the rest of the customer surface.
Delete is not idempotent: deleting an already-deleted recipe
returns 404, not 204.
Intent log assembly
Driftstack gathers the intents from every plan-executed agent turn
in the source session’s transcript into a single intent_log. The
result is captured once and never edited, so the snapshot survives any
later session activity. It reads the history the agent works from: in a
long session whose oldest entries were dropped from that history to make
room (see history under
agent sessions), the turns in the
dropped entries are not in the intent_log.
A step the agent planned but could not send — its result is a failed
step whose reason says it was not run — is left out, and so are the
steps planned after it in the same plan, which never ran. The turn’s own
intents still list them, but the intent_log never holds a step the
session refused to send, or a step without the one planned in front of
it.
Recipe payloads are encrypted at rest. Sensitive type values are
not stored, so what GET /v1/recipes/{id} returns is exactly what the
recipe holds: no API key, of any scope, can retrieve a password, a
one-time code, a saved credential or a card number from a recipe.
Only plan-executed agent turns carry intents — your own messages and
manual entries do not. A session that ran exclusively in mode='manual' will produce a
recipe with intent_count: 0 (manual sessions record what you did, not
an AI plan). That’s expected: the recipe still records the session it was saved
from.
Errors
| Status | Type | When |
|---|---|---|
| 400 | validation-failed | body fails schema (missing label, label > 120 chars, description > 2000) |
| 403 | forbidden | in a team owner’s workspace without the admin role, or X-Driftstack-Account names an account you are not a member of |
| 404 | not-found | agent_session_id doesn’t exist, or belongs to an account other than the workspace you save into — 404 rather than 403 so the response does not confirm the session exists |
| 409 | conflict | the session is already saved as the recipe in recipe_id, under another label or description |
| 429 | tier-limit | the account already keeps as many recipes as its plan allows |
| 401 | unauthorized | missing or invalid bearer token |
| 503 | feature-unavailable | recipe storage is not enabled for this deployment |