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.kind | Shape |
|---|---|
contact | {contactId} |
contacts | {contactIds: [...]} |
rows | {tableId, rowIds: [...]} |
view | {tableId, viewId} or {tableId, filter} |
records | Inline data not in the workspace yet (read and spend classes only). |
none | Operations 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.
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
| Field | What it tells you |
|---|---|
targets | total, wouldRun, and wouldSkip grouped by reason with a sample. |
credits | estimatedMin and estimatedMax, what the wallet has available, and what is left of the key's daily credit cap. |
quota | LinkedIn 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. |
approval | Whether a person must approve, and why (key mode, operation floor, over threshold, agent, ...). |
writes | What would be written: table rows, contacts, CRM records and conflicts. |
preview | The first targets as they would run: rendered message, mapped CRM fields, query. |
planId · expiresAt | The 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_STALEand runs nothing.error.details.changedsays what moved anderror.details.planis the new plan: review it, then apply the newplanIdwith a newIdempotency-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.