Drıftstack docs
Docs

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’s next_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