Guides

Authoring recipes

Write a recipe as readable JSON — steps, values, conditions, AI functions, and the ways a recipe can run.

A recipe is a JSON description of work: fetch data, compute over it, render or act on the result. There is no code in a recipe — transforms are pure functions, ingredients are declared operations, and every step's inputs and outputs are inspectable. You author and test recipes in the Kitchen, then keep them local or publish them.

#Anatomy

Ingredient slugs and fields below are illustrative; real ones come from what you have installed.

json
{
  "recipe_id": "digest-unanswered-email",
  "version": 1,
  "ttl": 600,
  "metadata": {
    "name": "Unanswered email digest",
    "description": "Summarize threads waiting on a reply from you.",
    "author": "you",
    "tags": ["email", "digest"]
  },
  "variables": {
    "days": 3
  },
  "steps": [
    { "id": "mail", "ingredient": "mail-reader",
      "input": { "folder": "inbox", "within_days": "{{config.days}}" } },
    { "id": "waiting", "transform": "filter",
      "array": "{{step.mail.messages}}",
      "field": "answered", "operator": "equal", "value": false },
    { "id": "summary", "ingredient": "ai-summarize",
      "input": { "llm.data": "{{step.waiting}}",
                 "llm.focus": "who is waiting on me" },
      "skip_when": "{{step.waiting}} is_empty" }
  ],
  "output": {
    "render": [
      { "type": "ai_analysis", "source": "step.summary" }
    ]
  }
}

variables become {{config.*}} — the arguments a caller fills. output.render routes step results into typed display sections; a null source is simply not rendered. Note the two parameter placements: transform parameters sit flat on the step (array, field, …), while ingredient parameters go under input with dotted keys such as llm.data.

#Variables: what gets asked, and what you tuned

How you write a variable says who it is for.

json
"variables": {
  "recipient": { "label": "Send to", "type": "text" },
  "days": 3
}

A variable written as an object with a label is a question for whoever runs the recipe — it gets its own field on the run form. A variable written as a bare value is a constant you tuned, and the run form leaves it alone.

Both stay editable. A tuning constant still appears wherever tuning belongs — the install dialog, the recipe editor, and the Schedule and Trigger tabs, because "run this nightly with days: 30" is a setup decision. It is only the one-shot run form that stops asking.

Field Meaning
label Required on the object form. The text shown, and the signal that this is a question
type Required. text, number, boolean, enum, datetime, url, secret, and others
optional true lets the runner leave it empty. Leave it out to require a value
default used when nothing is supplied
help one line under the field
options for type: "enum" — the choices; the first is the default

Two rules worth knowing before you hit them:

  • Only these fields exist. Anything else is refused when you install, so a field you invent fails loudly instead of being quietly ignored. In particular there is no requiredoptional is the one that exists, and leaving it out already means required. (A great many recipes carried a required field that never did anything, which is exactly why it is refused now.)
  • A caller can only pass variables you declared. If a chat request, an MCP call, or a button hands your recipe a name that is not in variables, the run is refused rather than silently ignoring it. So every argument you want to accept has to be declared — and a leftover name you no longer read has to go.

To validate the shape of a value — an email address, a range — use a guard step. Declarations say what a value is called and how it is collected, never what counts as valid.

#How a step executes

Every step follows the same flow:

text
skip_when? → compute → step.<id> = result → fail_on?
  • skip_when evaluates before execution; a skipped step stores null and the recipe continues.
  • The step computes: a transform (pure function), an ingredient (declared operation), or a guard (halts the recipe when its condition is met).
  • The result lands on {{step.<id>}} for later steps.
  • fail_on evaluates after; a match halts the recipe.

foreach runs a step once per item of an array, with {{item.*}} bound inside each iteration.

#Values and conditions

text
"literal"               used as-is
"{{ref}}"               resolved, type preserved
"text {{ref}} text"     interpolated into a string
"{{ref:currency}}"      format hint, applied only during interpolation
10, true, null          native JSON (null = required from caller)

Conditions are inline strings with exactly one operator:

text
"{{step.deal}} is_null"
"{{step.days}} greater {{config.threshold}}"
"{{step.stage}} equal closed_won"

Two rules save the most debugging time. Literals are bare — write equal closed_won, never equal 'closed_won'; a quoted literal never matches. And compound logic is a transform step (any / all) whose stored boolean you check — and / or do not exist inside condition strings. The full operator table is in the recipe schema reference.

#Compute with transforms

Around fifty pure transforms cover collections (filter, sort, map, group_by), objects, strings, dates, numbers, and logic. Prefer a chain of small deterministic steps; reach for AI only where judgment is genuinely needed.

#AI steps

Use contracted AI functions — ai-classify, ai-score, ai-extract, ai-summarize, and friends — before ai-prompt. Contracted shapes validate, and every function except ai-compare can process an array in one batched call. Ask for model quality by hint (fast, quality, thinking), not by provider. See AI and models.

#Ways a recipe runs

Mode Declared by
Manual Nothing — every recipe can be run by hand
Page-triggered trigger: URL patterns of pages the recipe is designed for
Reactive auto_run interval plus an optional trigger_steps gate
Scheduled A cron schedule on the server

Reactive recipes gate themselves: trigger_steps run on every tick, and each step must return should_run: true for the run to proceed — a quiet tick leaves no trace. A circuit breaker disables a recipe after repeated consecutive failures and notifies you; you re-enable it deliberately.

#Test, fork, publish

The Kitchen validates against the schema as you edit and runs the recipe against your server. Forking a marketplace recipe keeps it local; publishing a fork gives it your publisher handle and its own credential scope, so same-named recipes from different publishers never share secrets. Publishing requires a free account; browsing and installing never do. The full flow — validation, review, versions, unpublishing — is in Publishing.

When a recipe needs an operation that is not already installed, do not inline credentials or an ad hoc request in the recipe. Build a governed capability in Authoring ingredient packs, install it locally, then reference its operation from the recipe.

Recued is local first; your server remains the authority.

Recued Docs

Search documentation

Start typing to search the documentation.