How It Runs

String in, string out — AI inside apps handles the rest

Most workflow tools make you do plumbing. Map this field to that field. Define the output schema of step 1 so step 2 knows what to expect. Change one step and watch three downstream connectors break.

Pipedown throws all of that away. The execution model is radically simple:

Every step receives a string. Every step returns a string. That’s the entire contract between steps.

No schemas. No field mapping. No type negotiation. Just text flowing through a pipe, with AI inside each app handling the interpretation. This is the key design insight that makes everything else work.

Why strings?

Think about how you’d explain a task to a colleague over chat. You wouldn’t send them a JSON schema — you’d send them a message. “Here are the top payments from last week, can you post the highlights to #wins?”

That’s exactly how pipedown steps communicate. The output of /stripe payments this week is a human-readable summary. The next step — whether it’s an LLM step like filter to the top 3 or an app step like /slack post in #wins — reads that summary and figures out what to do with it.

This works because each app has AI built into its runtime. When /slack post in #wins top 3 highest selling receives Stripe’s output, it doesn’t need a field mapping. It calls something like:

const message = await tools.generate.text(
'sm',
`Given this sales data:\n${input.summary}\n\n` +
`The user wants: "${input.instruction}"\n\n` +
`Write a Slack message for the #wins channel.`,
)

The LLM inside the app handles the transformation. The pipe never knows or cares what shape the data is in.

Traditional workflows break when upstream steps change their output shape. Pipedown steps compose freely because each app interprets its own input.

This is also why plain-english steps work so naturally — summarize the above is just another step that receives a string, does something smart with it, and returns a string.

And it gets better: apps don’t call AI on every run. On first execution, the strategy system uses AI to generate a JS snippet that handles the mapping. It caches that snippet and replays it deterministically from then on — no AI call, instant execution.

The input object

Inside an action, the step’s context looks like this:

{
instruction: string // the args written in the pipedown line
summary: string // string output from the previous step
data: AppResultDataItem[] // structured data items from the previous step
}

instruction is what the flow author wrote — the arguments after /app action. summary is what the previous step returned — the string flowing through the pipe. data is structured items that ride alongside the string, each with type, key, value, and optional title, status, kind fields.

The string pipeline is always canonical. Data items are supplementary context, not a parallel channel — they’re there when an app needs structured access to the previous step’s output, but the string is what flows forward.

What the runner actually does

When a flow is triggered — manually, on schedule, or by an event — the runner walks through the steps one at a time. Each step gets the previous step’s output, does its thing, and hands off to the next.

The runner itself is simple. It maintains a handful of values as it moves through the pipeline:

  • lastOutput — the string output of the previous step (this is summary)
  • lastData — structured data items from the previous step
  • tagMap — named outputs from = variable assignments
  • contextObject — accumulated context across steps

Between steps, lastOutput flows forward automatically. That’s the pipe.

How steps get dispatched

Some steps are handled directly by the runner — these are the built-in constructs that control flow rather than doing work:

  • LLM steps (bare text) — sent to an AI model with the step’s text as the prompt and lastOutput as context
  • if / else-if / else — the condition and current output go to a lightweight model that answers YES/NO. Backtick syntax (if `input.includes('deploy')`) evaluates JS instead
  • cancel — same YES/NO evaluation; if YES, the pipeline stops cleanly
  • wait — serializes current state and pauses. A duration (/wait 2h) or human approval (/wait @person) resumes it later
  • message — posts to the flow’s output thread
  • --- separator — resets the conditional chain

App steps — anything starting with /app — get dispatched to the app sandbox, which is where things get interesting.

App sandboxing

Every app action runs in its own isolated sandbox. This isn’t just good practice — it’s fundamental to the model. Apps are code from different authors, potentially third-party, running with access to your workspace data. Isolation means:

  • One app can’t access another app’s memory or secrets
  • A buggy app can’t crash the runtime or affect other steps
  • Third-party apps get a controlled surface area, not full host access

Under the hood, each action runs in a fresh isolated-vm isolate — a V8 sandbox with its own heap, separate from the host process. The runtime keeps a small pool of reusable isolates so startup stays fast.

The sandbox gets a START_CONTEXT global with everything the app needs:

  • input{ instruction, summary, data } (its place in the pipe)
  • context{ serverId, channelId, threadId, object } (workspace info)
  • tools — the full platform API (see below)
  • oauthToken — if the app uses OAuth
  • options — install-time config set by the server admin

The isolate can’t reach host resources directly. When an app calls tools.data.public.set() or tools.generate.text(), those are callback bridges — the call goes out to the host, executes there, and the result comes back into the sandbox. Standard web APIs (fetch, URL, TextEncoder, Buffer, console, etc.) are polyfilled inside the sandbox.

For secrets: first-party apps can access env vars via tools.secrets. Third-party apps store credentials in tools.data.private — they never see your environment.

The tools API

Every action gets the same tools surface. This is the platform API that apps use to interact with the world. For full reference with code examples, see Tools API.

ToolWhat it does
tools.data.public / .privateStore and query structured data
tools.generate.text(tier, prompt)Generate text with model tier selection
tools.generate.object(tier, prompt, schema)Generate and parse structured JSON
tools.message.insert(opts)Post messages to channels
tools.notify / tools.notify.pushNotifications
tools.webhooksWebhook management (auto-registered via flow event triggers)
tools.abort(reason)Stop the pipeline cleanly
tools.progress(0-100)Update progress indicator
tools.flows.create(pipedown)Programmatically create flows
tools.helpers.slugify / randomIdUtilities
tools.crypto.signJWT / base64urlCrypto helpers
tools.files.*File system access (desktop only)
tools.strategy.object(opts)Cached AI to structured object (strategies)
tools.strategy.list(opts)Cached AI to array (strategies)
tools.strategy.data(opts)Cached AI to standardized data items (strategies)

The strategy methods deserve special attention — they’re how apps avoid calling AI on every run. See Strategies for the full story.

Parallel execution

Lines prefixed with * run concurrently. The runner groups consecutive parallel steps and fires them all at once:

* /stripe sales last week = sales * /posthog signups last week = signups * /github issues opened last week = issues --- summarize {{sales}}, {{signups}}, and {{issues}} into a weekly digest

All three fetches happen simultaneously. When the group finishes, outputs are joined and become the next step’s input. Variables assigned within parallel steps are available to later steps. Control-flow builtins (if, cancel, wait, etc.) can’t be parallel — they’re always sequential.

Error handling

Things fail. APIs go down, rate limits hit, data comes back weird. Pipedown gives you inline error handling right after any step:

/stripe payments this week ! retry 3 /github fetch open issues ! ignore /posthog events today ! fallback /notify alert posthog step failed
  • ! retry N — retry with exponential backoff (1s, 2s, 4s…)
  • ! ignore — swallow the failure, continue with empty output
  • ! fallback — run alternative steps instead

No handler and a step fails? The flow fails and stops. Apps can also call tools.abort(reason) to cleanly exit the pipeline on purpose.

Each step also has a 2-minute timeout. If a step runs longer than that, it’s cancelled and the flow fails. Break long operations into smaller steps or use tools.progress() to track progress within the limit.

The apps ecosystem

Here’s what makes pipedown genuinely extensible: the language doesn’t know about any specific service. /stripe, /slack, /linear are only valid because those apps are installed. The runtime never needs to know about Stripe or Slack directly.

This means:

  • Adding an integration adds vocabulary. Install the GitHub app and suddenly /github fetch issues works in any flow. No language changes needed.
  • Orgs can build internal apps. Your custom /deploy or /oncall app works exactly like a first-party integration.
  • AI knows what’s available. The flow assistant queries the app registry to discover installed apps before generating pipedown, so it only emits valid steps.
  • Descriptions drive discovery. Action description strings are what the AI uses to pick the right action. Good descriptions make AI-authored flows work correctly. See Creating Apps for the full app definition API.

Infrastructure

Flows aren’t just scripts that run and disappear. They’re persistent, visible, and auditable.

Persistence — flow and run records live in Postgres, synced to clients via Zero. The flow table holds pipedown source and trigger config. The flowRun table tracks each execution with status, step-by-step progress, and timing.

Execution logs — each flow gets a dedicated chat thread in the Flows channel. Step outputs post to the thread as the flow runs, giving everyone a real-time audit trail. You can watch a flow execute in chat just like you’d watch a colleague work.

Schedulingevery monday morning and every 15min triggers are backed by a Postgres-based scheduler. Supported frequencies: 5-minutes, 15-minutes, 30-minutes, hour, day, week, month.

Event triggerson stripe.payment fires when Stripe’s webhook handler returns a matching event. The event payload becomes the first step’s input (JSON stringified), so you’re immediately working with real data.

Going deeper

This page covers how flows work from a user’s perspective. If you want to go further:

  • Pipedown Runtime — the abstract execution model that any pipedown runtime implements, independent of start.chat
  • Implementation — start.chat-specific internals: runner state, step dispatch, isolated-vm pool details
  • Strategies — the learn-once-run-forever system that makes flows fast after first execution
  • Writing Flows — full syntax reference