Status: Developer preview.The fabric never lets a consequential action run unreviewed. When a run reaches a step your policy marks as gated, execution pauses — the run parks, waiting for a decision — instead of continuing on the assumption that someone would have said yes. Nothing gated executes without an explicit decision your application collects and posts back. This page walks the pause-and-resume lifecycle end to end, using one example that carries through this page, policy-engine.md, and budgets-and-limits.md.
Worked example: a dunning email
A finance-ops team has an agent task that chases overdue invoices. Partway through a run, the agent finds an invoice from Meridian Fabrication, 45 days overdue, and drafts a payment-reminder email tobilling@meridianfab.example naming the $4,280.00 balance owed. Sending
that email is a send_email call — outbound correspondence, to an external
party, naming a specific amount owed. That combination is exactly what a
policy pack gates rather than lets a run send unattended; see
policy-engine.md for how that decision actually gets
made.
The lifecycle, step by step
1. The run executes normally. Whether it’smode:"turn" (a synchronous
stream) or mode:"task" (a durable background run), the agent works through
its steps the same way — look up the invoice, draft the email — until it
proposes the one step that’s gated.
2. The run parks. Instead of calling send_email, the stream (or the
run’s event feed, GET /v1/runs/:id/events) emits an approval.required
event and stops making progress on that action:
mode:"task" the run’s own state also flips to waiting_approval —
visible on GET /v1/runs/:id — so polling the run instead of streaming it
surfaces the same pause.
3. Your app renders the payload — exactly as given. summary is the
canonical payload: not a hint, not a category label, the actual thing that
will happen if approved. Render it verbatim in your own UI — don’t
re-summarize it, truncate it, or paraphrase it into your own copy. What’s
shown to the approver, what the fabric recorded, and what the gate checks
before resuming all have to be the same payload (render == record ==
gate-match). See How to build an approval UI.
4. Someone decides. Approve or deny, through whatever review process your
application wraps around it — a tap, a Slack approval, a second reviewer,
whatever the action warrants.
5. Approve: post the digest back.
approvalId (which pending decision this is) and
summary (what it’s a decision about) — it doesn’t carry a field literally
named action_digest. The digest is the fingerprint the fabric computed for
the exact parked action; it’s what POST /v1/runs/:id/approve expects back,
checked against the action that’s still actually parked before anything
resumes. See the API reference for the full
request/response schema.
6. The run resumes from exactly where it paused. send_email executes,
and the stream or event feed continues.
Why digest binding matters
An approval is authority over one specific action instance, not a standing “yes, go ahead.” Binding the decision to a digest of the exact action closes two gaps a plainapproved: true flag would leave open:
- Nothing can be substituted underneath an approval. If the agent’s draft changes after someone has looked at it — a different recipient, a different amount, a retried attempt — the digest changes too, and an old approval no longer matches. The digest is checked before resuming; a mismatch is rejected, not silently applied to whatever is parked now.
- A stale or replayed approve call can’t be repointed. Because the check is against the digest that’s currently parked, replaying an old approval call against a run that has since moved on fails closed instead of executing something the approver never actually saw.
Expiry and TTL
Every parked approval carries an optionalexpiresAt; where it’s present,
treat it as authoritative. If no decision arrives in time, the pending
request stops being valid on its own — a decision that arrives after the
window closes is rejected, not applied retroactively. In a synchronous turn,
a timed-out approval surfaces as a terminal request.failed event whose
problem.code is approval_timed_out. In a durable task, the run’s
approval state moves to expired directly, visible on GET /v1/runs/:id.
Windows are sized to the action’s stakes, not fixed globally. Praxa’s own
production approval gates today range from 10 minutes — for time- and
price-sensitive actions — up to 30 minutes for multi-step purchase flows:
short enough that a stale approval can’t fire against a world that’s moved
on, long enough that someone has a real chance to see it. The policy engine
assigns the window per action (see policy-engine.md);
a longer window never authorizes anything different, only how long the
same already-parked action stays approvable.
If a run is also holding a spend reservation while parked, approving after
that reservation itself has expired is rejected outright — the fabric won’t
resume a run against a funding hold that’s no longer there. See
budgets-and-limits.md.
Deny
{"decision": "deny"} stops that action from ever executing. Denying is not
a retry signal — the run does not treat “no” as “try again and ask a second
time” on its own. What happens to the run afterward depends on whether it
can make progress without that step; it may end in a stopped state that’s
labeled for exactly what it is, rather than pushed forward past a decision
someone made on purpose. See “What happens at the ceiling” in
budgets-and-limits.md for how the fabric reports
outcomes that didn’t fully complete.
The pattern behind the preview label
“Developer preview” describes the public surface —/v1/runs, the digest
contract on /v1/execute, the SDK. It does not describe the underlying
discipline. Fail-closed approval gates, where what’s rendered, what’s
recorded, and what’s checked at resume time are required to match, are the
pattern Praxa’s own consumer product runs today, in production, across
eleven separate approval domains — purchases, browser actions, connector
writes, social posts, credential changes, and more. The fabric’s unified
policy engine is what’s consolidating those eleven gates into one; the gate
itself, refusing to execute the unreviewed, is not new. See
Deployment and Isolation
for the full trust story.
See also
- Policy engine — how an action’s risk tier gets decided
- Budgets and limits — spend reservations and what happens at a ceiling
- Events — the full event catalog and stream framing
- Execute — submitting the run that eventually parks