Durable recipes
TypeScript-defined workflow logic with conditionals, branching, and resumable state.
A durable recipe is a TypeScript function that orchestrates an Oxygen workflow with programming-language control flow. Use a recipe when the motion needs conditionals, branching, or repeated steps that a template cannot express cleanly.
Shape
A recipe is a default-exported async function:
import type { RecipeContext } from "@oxygen/recipe-sdk";
export default async function myRecipe(
ctx: RecipeContext,
input: { sourceTable: string; limit: number }
) {
// ...
}ctx is the recipe runtime API. input is the workflow's call payload.
The ctx API
| Helper | Purpose |
|---|---|
ctx.tables.create({...}) | Create a table |
ctx.tables.upsert({...}) | Create or update a table |
ctx.columns.add({...}) | Add a column |
ctx.columns.run({...}) | Run a column over a row scope |
ctx.rows.upsert({...}) | Write one row |
ctx.rows.upsertMany({...}) | Write many rows |
ctx.tools.run(toolId, input) | Call a provider tool |
ctx.approvals.require({...}) | Pause for approval |
ctx.events.emit(name, payload) | Fire a tenant event |
ctx.context.resolve(purpose) | Get workspace context |
Every helper accepts (or derives) a deterministic key that uniquely identifies the operation.
Determinism rules
Recipes must be replayable. The runtime records each step's outcome; on retry, completed steps short-circuit.
| Allowed | Not allowed |
|---|---|
Deterministic logic over input and prior step outputs | Random keys (UUIDs, Date.now() keys) |
Calling tools via ctx.tools.run | Raw fetch() to external systems |
Reading and writing tables via ctx.* | Filesystem, subprocesses, OS APIs |
| Conditional branches over fetched data | Top-level Promise.race with non-recipe promises |
A non-deterministic step key means the runtime will re-execute on retry and lose memoization. Use stable strings: "add-fit-score-column", not "step-" + Date.now().
AI column outputs
AI column outputs are JSON envelopes. To use them in a downstream tool step, extract the scalar field with a formula column:
await ctx.columns.add({
key: "extract-fit-score-int",
table: table.id,
name: "fit_score_int",
kind: "formula",
formula: "JSON.parse(row.icp_fit_score).score",
});Registering a recipe
oxygen workflows lint --file ./my-recipe.ts --json
oxygen workflows apply --file ./my-recipe.ts --json
oxygen workflows get <workflow-id> --json
oxygen workflows call <workflow-id> --input-json '{"sourceTable":"leads","limit":10}' --mode dry_run --json
oxygen workflows enable <workflow-id> --jsonapply compiles, validates, and registers the recipe as a workflow.
Runtime boundaries
Recipes should use Oxygen primitives for side effects: tables, columns, tools, context, approvals, and events. That keeps results observable as runs and cells instead of hiding work in local files or untracked network calls.