Guide 07
Async runs and events
Follow long actions with bounded long polling, per-target status and the event feed.
Last updated from API version 2026-10-06 at (live spec, refreshed hourly). openapi.json
Actions over more than one target, and every send, run async. The request answers 202 with the action and a Location: /api/v1/actions/{id} header; targets then run in batches. You follow them three ways: long polling, the per-target list, or the event feed (and webhooks built on it).
Lifecycle
| Action status | Meaning |
|---|---|
pending_approval | Waiting for a person in the app. |
queued · running | Accepted; targets are being worked. |
succeeded | Every target succeeded. |
partial | Some targets succeeded, others were skipped or failed. |
failed · rejected · canceled · expired | Terminal. Expired = approval not decided within 72 hours. |
Each target has its own status: queued, pending_approval, scheduled (with notBefore, waiting for a send slot), running, succeeded, skipped, failed or canceled, with a reason code and its result. action.counts sums them.
Bounded long polling
GET /actions/{id} with waitSeconds (0 to 30) holds the request until the action reaches a terminal status or the time is up, whichever comes first. Poll again for longer waits: a wait is never unbounded.
curl "https://app.owlbound.ai/api/v1/actions/$ACTION_ID?waitSeconds=30" \
-H "Authorization: Bearer $OWLBOUND_API_KEY"
curl "https://app.owlbound.ai/api/v1/actions/$ACTION_ID/targets?status=failed" \
-H "Authorization: Bearer $OWLBOUND_API_KEY"GET /actions/{id}/targets lists targets (filters status and reason), GET /actions/{id}/audit the per-target audit with the actor, and GET /actions every run, newest first. POST /actions/{id}/cancel cancels targets that have not started (a running target finishes); it is idempotent.
The event feed
GET /eventsis the workspace's event log, oldest first. Payloads are thin (ids, statuses, counts, reason codes): read the resource with its GET. Events are kept 30 days and enter the feed 2 seconds after they are written.
| Event | When |
|---|---|
action.started | An async action's first batch started. |
action.pending_approval | The action waits for a person. |
approval.decided | Approved, rejected or expired. |
action.target.completed | A target succeeded, or the runner skipped it (caps, credits). |
action.target.failed | A target failed. |
action.completed | Any terminal status, with counts. |
Target events are written only for actions created with notify: {targetEvents: true}, to keep volume down. Filter with types (comma separated) and actionId. meta.page.nextCursor is always set, also on the last page: keep it and poll with it for newer events.
let cursor = loadCursor(); // persisted between runs
for (;;) {
const url = new URL("https://app.owlbound.ai/api/v1/events");
url.searchParams.set("types", "action.completed,approval.decided");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.OWLBOUND_API_KEY}` },
});
const { data, meta } = await res.json();
for (const event of data) await handle(event);
cursor = meta.page.nextCursor;
saveCursor(cursor);
if (!meta.page.hasMore) await new Promise((r) => setTimeout(r, 5000));
}Prefer push? Webhooks deliver the same events to your endpoint. The feed stays the source of truth.