OXYGENOxygen/ Docs
Authoring

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

HelperPurpose
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.

AllowedNot allowed
Deterministic logic over input and prior step outputsRandom keys (UUIDs, Date.now() keys)
Calling tools via ctx.tools.runRaw fetch() to external systems
Reading and writing tables via ctx.*Filesystem, subprocesses, OS APIs
Conditional branches over fetched dataTop-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> --json

apply 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.

  • Workflows — what a recipe compiles to.
  • Templates — when a parameterized template is enough.
  • Approvalsctx.approvals.require semantics.

On this page