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:
{
"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
{
"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:
skip_when? → compute → step.<id> = result → fail_on?- If
skip_whenis true, the step storesnull. The recipe goes on. fail_onis checked after the step computes. If it is true, the recipe stops.foreachruns 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 storesnullinstead 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.
"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
"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 callerThe 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:
{ "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
"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
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-exaThe platform always goes last. That is what lets variant_group fold the
copies made for different platforms into one marketplace card.