Browse the docs

Guide 04

Dry runs, plans and applying

See exactly what an action would do, then run that exact plan, or nothing.

Last updated from API version 2026-10-06 at (live spec, refreshed hourly). openapi.json

Every action operation supports a dry run. It resolves the targets and runs every check a real call would run, up to the first side effect, then stops. Nothing is reserved, sent, written or charged. Use it to show a person (or your own code) exactly what will happen, then run that, or nothing.

The request

Every action takes the same body: target (who or what it acts on), input(the operation's own fields), and optional dryRun, planId, mode, maxCredits, identityId, notify, output.

target.kindShape
contact{contactId}
contacts{contactIds: [...]}
rows{tableId, rowIds: [...]}
view{tableId, viewId} or {tableId, filter}
recordsInline data not in the workspace yet (read and spend classes only).
noneOperations that produce targets, such as lists.

Each operation lists the target kinds it accepts and its maximum. Set dryRun: true in the body, or send the header Owlbound-Dry-Run: true.

bash
curl -X POST "https://app.owlbound.ai/api/v1/actions/linkedin/connect" \
  -H "Authorization: Bearer $OWLBOUND_API_KEY" \
  -H "Owlbound-Version: 2026-10-06" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "target": {
    "kind": "view",
    "tableId": "<tableId>",
    "viewId": "<viewId>"
  },
  "input": {
    "note": "Hi {{firstName}}, saw your team is hiring SDRs in Lyon."
  },
  "dryRun": true
}'

The plan

FieldWhat it tells you
targetstotal, wouldRun, and wouldSkip grouped by reason with a sample.
creditsestimatedMin and estimatedMax, what the wallet has available, and what is left of the key's daily credit cap.
quotaLinkedIn sends: the identity's daily budget left, the key's daily sends left, how many would wait for a later day and when the first and last would go.
approvalWhether a person must approve, and why (key mode, operation floor, over threshold, agent, ...).
writesWhat would be written: table rows, contacts, CRM records and conflicts.
previewThe first targets as they would run: rendered message, mapped CRM fields, query.
planId · expiresAtThe plan id, valid 15 minutes.

Apply the plan

Send the same request with planId in the body (or the Owlbound-Plan header) and without dryRun. The server checks that the input, the target list, the skip set, the approval outcome and the credit maximum still match.

  • If anything changed (a contact replied or was suppressed, a price moved) or the plan expired, the call is 409 PLAN_STALE and runs nothing. error.details.changed says what moved and error.details.plan is the new plan: review it, then apply the new planId with a new Idempotency-Key.
  • A plan runs once: a second apply is 409 PLAN_ALREADY_APPLIED. To read the outcome again, replay the request with its Idempotency-Key.
  • A plan belongs to the key that made it. Another key, or the new key after a rotation, gets 404: dry run again.
  • Send slots are not part of the match: they move with time and are decided at send time.

Inline or async

mode: "auto" (default) runs inline when the operation is sync, one target would run and the class is not send: the answer is 200 with {action, result}. Otherwise it creates an async run: 202 with the action (see async runs and events). mode: "sync" forces inline (422 ACTION_NOT_SYNC when it cannot), "async" forces a run.

The CLI and MCP dry run by default

The CLI prints a plan unless you pass --apply, and MCP tools only plan unless called again with apply: true and the plan id.