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. It also covers, marked or not, a type step whose selector
names no field or does not name one specific field (an #id, or an
[id=…], [name=…], [aria-label=…] or [placeholder=…] attribute,
or a text= selector) and a press step whose value is not a key
name (a key that types a character, Space included, or other text).
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; to keep those steps in full,
save the recipe again while a new session runs them:
"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.
The detail is a display copy: in every string of every step (a
navigate or wait url, a selector, a typed value, a prompt) the
secret parts of an address read as [redacted]: a password in the
address (https://[redacted]@host/, http://host/user:[redacted]@proxy),
and secret query values and tokens (?token=[redacted]). The rest of
each step is unchanged. Driftstack does not run saved recipes for you
today. A recipe whose steps relied on a password inside an address
does not work from this copy: supply that credential yourself, or use
a saved credential.
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 no API key, of any scope, can retrieve a password, a
one-time code, a saved credential or a card number from a recipe.
GET /v1/recipes/{id} returns the stored steps with secret address
parts masked as described under Get one.
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 |