porchopsdev
DocsYour first playbook
OpenPlaybook · tutorial

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:

triggers
[
  {
    "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.

steps[0] · kind: "agent"
{
  "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.

steps[1] · kind: "approval"
{
  "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:

steps[2] · kind: "terminator"
{
  "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:

guardrails
{
  "maxRunDurationSeconds": 300,
  "maxLlmCostCents": 0,
  "requiresApprovalFor": []
}

The whole document

Assembled, with the envelope (opbVersion plus the requires declaration) around the playbook:

approval-expiry.playbook.json
{
  "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 purpose and instructions in your own words; the runtime carries data between steps regardless of phrasing.
  • Give a step tools. Add names to its allowedTools list; see tools & capabilities for how names resolve.
  • Add a branch. A decision step routes on an expression over the prior step's output; see the format reference.