Skip to main content
px0’s trust model used to be binary. A tool either mutates something or it does not, and a workflow either may call it or may not. --dry-run stubs every write, which rehearses a workflow but never does the work. There was no middle - no way to say draft it and ask me - so anything that speaks in your name had to be handed the real capability up front, on the strength of a plan you read once. Approvals are that middle.
A held-back call is not executed. It is written down in full - the tool, its arguments, the run that drafted it, and what that run produced - and the model is told it has been queued, so the run still finishes and still produces its output. Only the part that leaves a mark waits.

Turning it on

Per workflow, in its frontmatter. This wins over the store-wide default in both directions:
Store-wide, for everything that does not say otherwise:
Read tools never wait. A queue that fills up with searches is one nobody reads, and the point of the gate is the calls that leave a mark.
Naming a tool the workflow cannot call fails validation rather than being ignored. The failure is silent in the dangerous direction: a misspelling would leave the tool you meant to hold back firing without asking, and nothing would say so.

Working the queue

px0 approvals show prints both the arguments and the output the run produced. Both, because a Slack message shown without the digest it announces is a decision made blind.
Approvals list oldest first - unlike every run listing in px0 - because this is a queue rather than a history, and the thing most likely to have gone stale is the thing you most need to see.
Approving calls the tool with the recorded arguments. It does not re-run the workflow. A re-run would draft something else against a later hour or a changed source, and the thing you read would never have been sent.
A call that fails when approved is left as failed rather than returned to the queue. Retrying is your decision, and one that silently went back to pending would be approved twice.

Editing before sending

“Right message, wrong channel” had one answer before this: reject it and run the workflow again, which drafts something else. The message you actually wanted was never sendable.
--set takes KEY=VALUE and is repeatable. A value that parses as JSON is read as JSON, so --set count=3 sets a number and --set channel=#ops sets a string. JSON that comes back from your editor unparseable changes nothing rather than being guessed at. Every edit is stamped on the approval and shown on its screen, because what goes out should never be silently different from what the run produced.

Answering from somewhere else

The queue notified you wherever you asked and could only be answered at the machine px0 runs on - the wrong shape for drafted writes, since approvals happen when you are away from the desk. px0 has no server, so this is polling. A read tool is asked what came back, and replies naming an approval are acted on:
A reply is recognised as approve apr_..., reject apr_..., or just yes apr_... / no apr_..., since that is what people actually type in answer to a message asking them a question. The verb has to open the reply, so “do not approve apr_…” matches nothing - and matching nothing does nothing.
Both settings are required together. A reply channel with no sender list is an approval queue that anyone able to post there could empty, so px0 refuses to run half-configured. A reply from anyone not on the list is logged and ignored.
Every reply goes through the same path a person at the terminal takes: an expired draft stays expired, an answered one is not answered twice, and the call that goes out is the one that was drafted. The daemon polls only while something is actually waiting.

Going stale

A drafted message is written against a moment. Sending last Tuesday’s standup on Friday because it was still sitting in the queue is worse than not sending it, so a draft older than approvals.expire_days (7 by default) stops being approvable. Set it to 0 to never expire. px0 status counts what is waiting, and an unattended run that drafts something tells you through the same channels a failure uses. Nobody was there to see it happen, and silence would leave you believing the message went out. A manual run is not notified about: you are there, and it prints the draft.

Settings