Your first playbook
Three steps, one trigger, one human gate: when a charge fails, draft an apology email and hold it for the operator's decision. The document below is real; it runs in the spec's own conformance suite.
The idea
A payment fails. An agent drafts a short apology email to the customer. The draft does not send itself: it holds at an approval gate until a human decides, and if nobody decides within a week, the gate expires and the run ends. Draft, gate, done.
1. The trigger
A webhook trigger names its source and event. Here the run starts when the payments source reports a failed charge:
[
{
"kind": "webhook",
"source": "payments",
"event": "charge.failed"
}
]2. The agent step
An agent step declares its work as a plain-English purpose. This one allows no tools (the draft is pure writing), runs under a support role, and halts the run if it fails. Its next edge names the step that follows: control flow in OpenPlaybook is always an explicit edge, never implied order.
{
"id": "draft",
"kind": "agent",
"role": "support",
"purpose": "Draft a short apology email to the customer email address named in the trigger payload, as a single email_draft artifact.",
"allowedTools": [],
"approvalPolicy": {
"mode": "auto"
},
"failureMode": "halt_run",
"next": "gate"
}3. The approval gate
The gate holds the drafted email for a human. The operator approves, rejects, or does nothing; after the timeout (seven days here) the gate expires and the run terminates. An expired gate is terminal, never an implicit approve.
{
"id": "gate",
"kind": "approval",
"approver": "operator",
"timeout": "7d",
"onTimeout": "cancel",
"purpose": "Hold the drafted email for the operator's decision before the run may continue.",
"next": "done"
}4. The terminator
Every path ends at a terminator, with a reason:
{
"id": "done",
"kind": "terminator",
"reason": "The operator approved (never reached in this vector).",
"purpose": "End the run after approval; unreachable - the gate expires first."
}5. The guardrails
Hard limits cap the run regardless of what the steps do:
{
"maxRunDurationSeconds": 300,
"maxLlmCostCents": 0,
"requiresApprovalFor": []
}The whole document
Assembled, with the envelope (opbVersion plus the requires declaration) around the playbook:
{
"opbVersion": "1.6",
"requires": {
"connections": [],
"mcpServers": [],
"secrets": []
},
"playbook": {
"id": "pvr_D17KJBXF1JN52X4RR9XXQQ8825",
"playbookId": "plb_429B94127Y0TRRXKVC2FQ9RR25",
"opbVersion": "1.6",
"version": "1.0.0",
"status": "published",
"manifest": {
"id": "plb_429B94127Y0TRRXKVC2FQ9RR25",
"slug": "conf-approval-expiry",
"name": "Conformance: approval expiry",
"description": "A held email draft whose gate expires with no decision terminates the run.",
"author": {
"type": "porchops",
"id": "porchops",
"displayName": "PorchOps"
},
"category": "operations",
"requiredConnectors": [],
"requiredPermissions": [
"email.draft"
],
"visibility": "private",
"pricingTier": "free",
"premiumPriceCents": null
},
"instructions": "Draft the apology email, hold it at the gate; if the operator never decides, the gate expires and the run ends.",
"configSchema": {},
"tools": [],
"triggers": [
{
"kind": "webhook",
"source": "payments",
"event": "charge.failed"
}
],
"steps": [
{
"id": "draft",
"kind": "agent",
"role": "support",
"purpose": "Draft a short apology email to the customer email address named in the trigger payload, as a single email_draft artifact.",
"allowedTools": [],
"approvalPolicy": {
"mode": "auto"
},
"failureMode": "halt_run",
"next": "gate"
},
{
"id": "gate",
"kind": "approval",
"approver": "operator",
"timeout": "7d",
"onTimeout": "cancel",
"purpose": "Hold the drafted email for the operator's decision before the run may continue.",
"next": "done"
},
{
"id": "done",
"kind": "terminator",
"reason": "The operator approved (never reached in this vector).",
"purpose": "End the run after approval; unreachable - the gate expires first."
}
],
"guardrails": {
"maxRunDurationSeconds": 300,
"maxLlmCostCents": 0,
"requiresApprovalFor": []
}
}
}What to change to make it yours
- Swap the trigger. A schedule trigger runs on cron: Friday at 4pm is
{ "kind": "schedule", "cron": "0 16 * * 5" }. - Rewrite the
purposeandinstructionsin your own words; the runtime carries data between steps regardless of phrasing. - Give a step tools. Add names to its
allowedToolslist; see tools & capabilities for how names resolve. - Add a branch. A
decisionstep routes on an expression over the prior step's output; see the format reference.