Skip to main content
A workflow is one Markdown file under workflows/, at any depth - px0 loads *.md recursively. The YAML frontmatter is the machine contract; the body is the prompt the model receives. That is the whole format - there is no compile step, no lock file, and no hidden state. Edit the file and the next run uses it.

Frontmatter fields

description and request are not the same thing, and the distinction matters when you come back to a workflow months later. request is your words; description is the model’s normalization of them.

Watching instead of scheduling

trigger.watch fires on something happening rather than on the clock. px0 polls the named read-only tool at every (at least 60s), identifies each item by key (or by its own id, url, or number if no key is named), and runs the workflow when something new turns up:
The first poll only records a baseline, so adding a watch to a busy source does not immediately fire on everything already there. This is what a local-first tool can do in place of Composio’s own event triggers, which need a public endpoint to deliver to - something a laptop does not have.

Retries and failure notifications

retry controls how many times a failed run is attempted before it is recorded as failed for good, and how long to wait between attempts, doubling each time. Each attempt still writes its own run record, so px0 runs list shows the failures that led to an eventual success rather than hiding them. on_failure decides how you hear about it: a local desktop notification, a tool call such as slack.post_message or gmail.send_message, or nothing. A workflow’s own on_failure block wins over the store-wide notify.* config, which is what lets a noisy hourly job stay quiet while a nightly report shouts. See Configuration.

Inputs

Inputs run before the prompt and their results are interpolated into it. Think of them as the context-gathering phase: by the time the model reads the body, the data is already there. Each input has an id and exactly one of four sources:
Only read tools are allowed here. A write tool in inputs is a validation error, because inputs run unconditionally - a workflow should not post something just by starting up. Put write tools in tools instead, where the model decides whether to call them.args values are themselves templated, so an input can build on an earlier one.
Runs a retrieval query over brain/ and interpolates the matching passages, each prefixed with its path#anchor so the model can cite them. k defaults to retrieval.k_default (5).Anything under brain/work/ is excluded from this, as it is from every other retrieval px0 performs.
Filled from whatever you piped in with px0 workflows run <id> --stdin. stdin is the only supported source.
Runs another workflow and interpolates its output text. The sub-workflow’s output is routed to memory rather than to its own destination, so it feeds this prompt instead of being written or printed twice. It produces its own run record.

Optional inputs

By default, an input that fails stops the run - if the workflow is about those pull requests, producing a digest without them is worse than producing nothing. Mark an input optional: true and a failure instead resolves to nothing, the run continues, and the record marks it as degraded so you can see afterwards that something was missing.

Templating

The body and every args value are templated. Reference an input by its id:
A placeholder that is the whole value keeps its type, so an input holding a list stays a list. A placeholder inside a larger string is stringified in place. args are rendered against the context built so far, so a later input can use an earlier one: args: {query: "{{topic}}"}. Dotted lookups work for structured values and for the two built-in namespaces:

Guideline inlining

Guidelines named in guidelines: are read and inlined verbatim at run time. If the body contains {{guidelines}}, they go exactly there; otherwise they are prepended before the body. This is deterministic on purpose. Guidelines are matched by name, never retrieved by similarity, because a convention you rely on should not depend on a search hit. The price is that an irrelevant guideline in the list costs tokens and misleads the model, which is why the builder attaches one only when it genuinely matches the task. The run record notes which guideline files were inlined and at which version, so a run whose output looks off can be compared against the conventions in force at the time.

Output

Three rules govern path, and the reason for all of them is that the path can come from a model-written plan rather than from you:
  • It is always confined under output/. An absolute path, or a .. that climbs out, is rejected at run time rather than written. Everything else is resolved relative to the store’s output directory.
  • Only three placeholders are supported: {date}, {datetime}, and {time}. Both {date} and {{date}} styles work, since a plan that picked up the body’s {{...}} habit would otherwise produce a filename with literal braces in it. Any other placeholder is an error, not a filename. The default when path is omitted is output/output-{date}.md.
  • Concurrent writes are serialized with a store-wide lock, so two runs targeting the same path cannot interleave.
A workflow with trigger.schedule must use target: file. Nobody is watching stdout when a scheduled run fires, so px0 rejects that combination at validation time instead of dropping the output.

Pipelines

A workflow can be a sequence of other workflows instead of a prompt:
Each stage runs in order, piping its output into the next: every stage but the last writes to memory rather than to its own destination, and the last stage’s output is routed by the pipeline’s own output block. Each stage still produces its own run record alongside the pipeline’s. A stage that fails aborts the pipeline, and the run is recorded as failed. Pipelines cannot nest - a stage that is itself a pipeline is a validation error, which keeps the execution graph one level deep and legible.

Validation

Every run validates the file first, and the builder validates the plan before saving it. These are the checks, and each one exists because the alternative is a confusing failure much later:

A broken file does not break the rest

A workflow whose frontmatter does not parse is skipped, not fatal. One YAML typo used to take down every workflow command; now the other workflows keep working, and the broken file is reported by name and line - YAML’s own error reports the position as <unicode string>, which is no help at all.
The daemon survives it too: a file that fails to parse no longer stops the scheduler from firing everything else.

Editing by hand

Editing the file directly is expected and supported. The daemon’s nightly pass checkpoints your edits into version history, so they appear alongside px0’s own changes:
The one thing a hand edit cannot do is notice that your new instruction needs a tool the workflow does not have. When a change is substantive rather than cosmetic, px0 workflows edit re-derives the tools, inputs, and guidelines from your revised request and keeps them consistent. See Build a workflow.

Managing a workflow

Beyond building and running one, these act on the workflow file itself: