Reference

Recipe schema

The main recipe fields, and what each one means.

This page is a short list of the main recipe fields. It is for people who write recipes. Want the story version first? Read Authoring recipes.

#Envelope

The envelope is the outer shell of a recipe. Here is one with the fields this page covers:

json
{
  "recipe_id": "detect-deal-risk-hubspot",
  "version": 1,
  "ttl": 600,
  "metadata": {},
  "variables": {},
  "requires": [],
  "depends_on": [],
  "chat_exposed": false,
  "trigger": [],
  "event_triggers": [],
  "webhook_triggers": [],
  "auto_run": null,
  "trigger_steps": null,
  "run_mode": null,
  "provenance": true,
  "prefetch_steps": [],
  "steps": [],
  "output": {}
}

#Envelope fields

Field Type Notes
recipe_id string The recipe's name. Its shape is {action}-{entity}-{qualifier}[-{platform}]. The platform always goes last
version number A whole number. It says which version of this recipe this is
ttl number How many seconds a fetched answer may be kept and reused. It is the default for ingredient fetches
metadata object Display and marketplace fields. See the table below
variables object Values the user can set. Steps read them as {{config.*}}
requires string[] Permissions the recipe needs, such as notification_send or mail_send
depends_on string[] Packs whose operations the steps call. Write each one as publisher.pack. Built-in core.* operations need no entry
chat_exposed boolean Let chat and connected AI agents use this recipe as a tool. See Exposing recipes to chat

#Envelope fields for triggers, steps, and output

Field Type Notes
trigger string[] URL patterns of the web pages this recipe is made for. Leave it out, and the recipe runs by hand only
event_triggers object[] Warehouse events (a record in your warehouse changing) that start the recipe. See Triggers and watchers
webhook_triggers object[] Incoming webhooks that start the recipe. Each one names a verification profile (the named way the sender proves the message is real)
auto_run object Makes the recipe tick by itself: { "interval_ms": 60000, "dynamic": false }
trigger_steps step[] Steps that run before each tick and decide if the tick goes on. Needs auto_run
run_mode string live, backfill, or manual. It tags the audit entries
provenance boolean Default true. It writes the links that timelines are built from
prefetch_steps step[] Steps that all run at the same time, before the main steps
steps step[] The main steps. They run one after another
output object The render sections. They send step results to the screen

#metadata

Field Notes
name, description What people see: the name, and what the recipe does
author The publisher handle. That is your name in the marketplace
tags Any words you like. They help people find and filter recipes
supported_platforms The platforms this copy of the recipe is made for
variant_group Groups the copies of one recipe made for different platforms
execution_scope You can skip it. It narrows where the recipe may run. It must stay inside what the ingredient kinds already allow
budget_ms The most time a run may take, in milliseconds. The engine stops a run that goes past it. Set one on everything you ship
fork_of Where a published fork came from: the original recipe, its author, and its version
min_recued_version You can skip it. The lowest Recued this recipe runs on, such as 26.10.12. Name a release that has shipped. A server older than that will not install or update the recipe, and says to update Recued. Only servers that know this field check it

#Steps

json
{
  "id": "risky",
  "transform": "filter",
  "array": "{{step.deals}}",
  "field": "stage",
  "operator": "equal",
  "value": "at_risk",
  "skip_when": "{{step.deals}} is_empty"
}

Recued tells the kind of step from which field is present:

Kind Field What it does
Transform transform A pure local function. Its settings sit right on the step. See Transforms
Ingredient ingredient A declared operation: HTTP, DOM, AI, chat, MCP, service, storage, or connection. Its settings go under input, with dotted keys such as llm.data
Operation op A named operation. It is built in (core.mail.get, core.notification.send, core.watch.time, …) or it comes from an installed pack (publisher.pack.entity.verb, listed in depends_on). Its arguments go under args. It names no connection: it uses the one the owner chose for the pack it calls, else that pack's default
Guard guard The field holds a condition. When the condition is true, the recipe stops
Condition condition Checks a condition. Then it runs the then steps or the else steps

#Step execution flow

Every step runs the same way:

text
skip_when? → compute → step.<id> = result → fail_on?
  • If skip_when is true, the step stores null. The recipe goes on.
  • fail_on is checked after the step computes. If it is true, the recipe stops.
  • foreach runs the step once for each item in a list. Inside, {{item.*}} is that item. The output is one envelope per item: { ok, result?, error?, item }. They keep the input's order.
  • A prefetch step can also say "optional": true. Then a failure stores null instead of stopping the run.

Trigger steps (trigger_steps) have the same shape. Every one must return should_run: true, or the tick stops. Their outputs land on {{trigger.<id>.*}} for the steps after them.

#Variables

A variable has two forms. A bare value is a setting you tuned. Recued works out its type. The one-time run form does not ask about it. An object is a question for the person who runs the recipe. It gets its own box on that form.

json
"variables": {
  "recipient":  { "label": "Send to", "type": "text" },
  "window":     { "label": "Window", "type": "enum", "options": ["7d", "30d"] },
  "max_rows":   50,
  "deal_id":    null
}
Bare value Inferred Control
50 number A number box
true / false boolean An on/off switch
"text" string A text box
["a", "b"] enum A drop-down list. The first one is the default
null string, required A text box with no default. The recipe cannot run until you fill it in

You can still change a tuned setting in the places where tuning belongs. Those are the install dialog, the recipe editor, and the Schedule and Trigger tabs. There, "run every night with window: 30d" is a real setup choice. Only the one-time run form stops asking.

#Object-form fields

These are the only fields there are. Any other field is refused when you install. So a made-up field fails loudly. It is never quietly ignored.

Field Notes
label Required. The words shown. It also tells Recued this is a question, not a setting
type Required. A string that is not empty. See the table below
optional true lets the person leave it empty. Leave it out to require a value
default Used when nothing is given
help One line of help under the box
link A web link to more help
options For type: "enum". The choices. The first one is the default
provider, scopes For type: "oauth". Which broker handles the sign-in, and which scopes to ask for
accept_mime_types For type: "file_ref". It only narrows the file picker. It is not a safety wall
connection_kind For type: "connection". notification or mcp. A recipe asks for no api one
entity For type: "record_ref". Which kind of stored row the picker searches. It only narrows the picker. It is not a safety wall
entity_filter For type: "record_ref". More matches on top of entity, like { "root_ref": "…" }. It only narrows the picker, too
starter For type: "mail_template". The template you built and tested the recipe with. Installing the recipe makes it. See Authoring triggers

There is no required field. The optional field is the one that exists. Leave it out, and the value is already required. There is no pattern field either. To check the shape of a value, use a guard step. A variable says what a value is called and how to collect it. It never says what counts as valid.

#Types

The field list is closed, but type is open. These types each get their own kind of box:

text · number · boolean · enum (with options) · datetime · date · url · secret · oauth · file_slug · file_ref · file_ref[] · record_ref · mail_template

datetime is one moment. The form sends it with the time zone of the person filling it in, like 2026-10-01T09:30:00+02:00. date is a day, like 2026-10-01. It has no time and no time zone. Use it for a due date or a target date.

record_ref is one of your stored rows. You pick it from a search. mail_template is one of your mail templates. It is a setting of the recipe, never a question for one run.

These are allowed too, and many recipes use them. The form shows each of them as a plain text box. An array with options shows boxes to tick instead:

string · array · object · json · connection

string is the most common type in shipped recipes. It works like text. Any other non-empty string installs and shows as text. So the list can grow, and the recipes you already published still work. That is why an unknown type is allowed but an unknown field is not. A type has a safe fallback. A field nobody reads has none.

#A caller may only pass what you declared

A chat request, an MCP call, a button, or a schedule can pass values to your recipe. If it uses a name that is not in variables, the run is refused. The name is not ignored. So declare every argument you want to accept. And remove any old name you no longer read.

Give a default only when a good one exists. A required input with a filler default is not really required. An empty string, a zero, or an empty list is a filler. The caller can leave it out, and the recipe runs with the filler.

#Value system

text
"literal"              → use as-is
"{{ref}}"              → resolve, preserve type
"{{ref:format}}"       → resolve; the hint applies only during interpolation
"text {{ref}} text"    → interpolate → string
10, true, false        → native JSON type
null                   → required from caller

The format hints are :currency, :number, :date, :relative, and :percent.

#Conditions

A condition is a short string. One condition per field. One operator per condition. Plain values have no quotes: write equal closed_won, never equal 'closed_won'.

Operator Form
equal, not_equal two sides (binary)
greater, greater_or_equal, less, less_or_equal two sides (binary)
is_null, is_not_null, is_empty, is_not_empty one side (unary)
contains, not_contains two sides (binary)
in, not_in two sides, and the right side is a list. Use the object form

The object form, for a list value:

json
{ "field": "{{step.x}}", "operator": "in", "value": ["a", "b"] }

Want to join conditions? Use a transform step, any or all. It stores a true or false. Check that. There is no and or or inside a condition string.

#Namespaces recipes can reference

A namespace is the first part of a {{ref}}. It says where the value comes from.

Namespace Contents
config The recipe's variables. They are set at install or at run time
context Facts about the caller, plus fields the engine adds (see below)
meta The recipe's metadata
step Every step's output, both prefetch and main steps
trigger The trigger steps' outputs, on recipes that tick by themselves
item The current item inside a foreach
shared Records you can write that live in the cache
data.shared Records you can write that are kept for good
data.memory Audit and memory entries. Needs the read-memory permission
data.contact Your contacts. Each one is keyed by its main email address
connection The non-secret fields of named connections

Only ingredients may use {{vault.*}}. A recipe never sees a secret key. The vendor aliases data.hubspot.*, data.salesforce.*, and data.crm.* read enrichments on CRM records. They work with fixed ids only.

#Context fields the engine adds

Field Meaning
context.tabs The ingredient slugs whose URL patterns match a browser tab that is open
context.server { available, name? }. Can the paired server be reached right now?
context.event The event that fired the recipe, on a reactive run (a run started by an event, not by you). On updates, payload.prev holds the old values of the hot fields (the fields that change most)
context.recipe.* Step results saved from the last run, so one run can pick up from the one before. Not set on the first run
context.connections The connection the run used for each pack it called, by the pack's connection name: {{context.connections.hubspot}}, or {{context.connections.crm}} for a core.crm.* step. Use it to keep each connection's records apart. Left out when the run used more than one there, as a search that reads every connection does: read the record's own connection_name instead. The server sets it; a caller cannot

#Transforms

Each transform's settings are listed in the transforms reference. Tier 1 is the small building blocks:

Group Transforms
Collection filter, sort, map, reduce, unique, flatten, slice, group_by, to_list, partition
Object merge, pick, omit, rename, set
String lowercase, uppercase, trim, split, concat, replace, template, truncate, strip_html
Numeric round, clamp, to_number, math, weighted_score
Date date_diff, date_format, date_add, date_parse, is_past, is_future, date_period, event_when
Logic compare, coalesce, switch, all, any, count, default, not, ternary, pluralize
Boolean starts_with, ends_with
Privacy hash_replace, hash_restore, redact
Display to_checklist, to_table, to_summary, to_csv

Tier 2 is handy shortcuts built from Tier 1: find, pluck, sum, min_by, max_by, percent, join, and group_by with aggregate.

Two modes are worth knowing. The filter transform takes mode: "all" | "any" plus a conditions list. That checks many conditions in one step. The map transform takes an expression over {{item.*}}. The expression can be one ref, a math expression, or an object template. Add output_field to attach the answer to each item instead of replacing the item.

#AI functions

Function Input Output
ai-classify llm.data, llm.categories[], llm.context? { category, confidence, reasoning }
ai-score llm.data, llm.criteria[], llm.scale? { score, breakdown[], reasoning }
ai-extract llm.data, llm.fields[] { [field]: value }
ai-summarize llm.data, llm.max_length?, llm.focus? { summary, key_points[] }
ai-sentiment llm.data { sentiment, score, signals[] }
ai-compare llm.data_a, llm.data_b, llm.dimensions[]? { differences[], similarities[], recommendation }
ai-generate llm.data, llm.template_type, llm.tone? { content }
ai-translate llm.data, llm.target_language { translated, source_language, confidence }
ai-rewrite llm.data, llm.style, llm.instructions? { rewritten }
ai-prompt llm.system_prompt, llm.prompt, llm.output_format?, llm.model_hint?, llm.allow_search? uncontracted

Batch mode: every function except ai-compare can take a list as llm.data, plus a non-empty llm.id_field. That is one model call. It gives back one answer per item, in the same order as the input. Each answer is the source record merged with the result fields. The model hints are fast, quality, and thinking.

#Output

json
"output": {
  "render": [
    { "type": "summary", "source": "step.overview", "label": "Overview" },
    { "type": "ai_analysis", "source": "step.analysis" }
  ]
}

This only routes. There is no logic here. A null source is not shown. The section types:

Type Renders
checklist The output of to_checklist, as pass, fail, or neutral items
table The output of to_table, as a table
summary The output of to_summary, as label and value pairs
ai_analysis An AI answer, with a badge that shows how sure it is
text Any string
copyable A string with a copy button
button Buttons the owner clicks. Each one opens a normal recipe run, with all the usual checks
file_artifact One or more file cards. You must be signed in to preview or download
link_button A plain link out: a label plus an HTTPS URL. It is the one block a public page for visitors can act on
json Raw data from a step. It is the detail behind a tidy card. Each surface shows it its own way: the web folds it up, a chat or agent caller gets it word for word
filter A form built from your declared variables. Submit it, and the whole recipe runs again with those values. Owner only. A page for visitors leaves this block out

#How output reaches a reader

render is the only way a run's data reaches anyone. A step result that is not sent to a section cannot be seen. Not by a person. Not by a model. Use json when a reader needs to check the work behind a summary.

The filter block is the one section that is not { type, source, label }. It also has three more fields: fields, hidden, and submit. The fields list names the declared variable keys to show as boxes. The hidden list names declared keys that are sent on submit but never shown. A paging cursor (a marker for the next page of results) is the usual one. The submit field is the button text. Every box comes from the variable's own declaration. There is no styling to write.

#When Recued refuses your recipe

When a recipe is refused, Recued names what it did not like. Here are the ones authors meet most:

Refusal Cause
output_unknown_key, output_section_unknown_key A made-up field on output or on a section. label is the display field. There is no title, content, or body
variable_hint_invalid An object-form variable with no label or no type, or an empty one
variable_hint_unknown_key A field on a variable that does not exist. Most often it is required, which was never a field
undeclared_variable_ref The recipe reads {{config.x}}, but there is no x in variables
undeclared_config_argument A caller sent a config key the recipe never declared. This happens at run time, not at install
unknown_transform A transform name Recued does not know
condition_operator_invalid An operator that is not one of the fourteen
forward_step_ref A step reads a step that runs later
nested_template A {{ }} inside another {{ }}
context_recipe_in_auto_run {{context.recipe.*}} in a recipe that ticks by itself. It carries last-run state on manual and scheduled runs only
filter_variable_undeclared, filter_field_not_labeled A filter block names a key that is not declared, or one that is a bare setting instead of a labelled question

orphan_step and ttl_below_floor are warnings, not refusals. The first is a step nothing reads. The second is a cache TTL that is lower than the practical floor.

#Naming

text
Recipe:      {action}-{entity}-{qualifier}-{platform}    detect-deal-risk-hubspot
Ingredient:  {action}-{entity}-{platform}                deal-reader-hubspot
AI:          ai-{function}                               ai-score
Vendor:      {action}-{vendor}                           search-exa

The platform always goes last. That is what lets variant_group fold the copies made for different platforms into one marketplace card.

Recued Docs

Search documentation

Start typing to search the documentation.