Browse the docs

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 statusMeaning
pending_approvalWaiting for a person in the app.
queued · runningAccepted; targets are being worked.
succeededEvery target succeeded.
partialSome targets succeeded, others were skipped or failed.
failed · rejected · canceled · expiredTerminal. 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.

EventWhen
action.startedAn async action's first batch started.
action.pending_approvalThe action waits for a person.
approval.decidedApproved, rejected or expired.
action.target.completedA target succeeded, or the runner skipped it (caps, credits).
action.target.failedA target failed.
action.completedAny 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.

Tail the feed
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.