Q for developers

SDK workflow helpers

Wait for indexed documents, observe agent outcomes and recover existing runs from saved receipts.

SDK workflow helpers

The Python and TypeScript SDKs provide bounded, read-only lifecycle helpers. They use your client's configured API origin and credentials. They never start replacement runs, approve actions, submit tool results, or cancel server work automatically.

These helpers are available in the repository SDK source and qualified archives. Check SDK support for public package availability before relying on a registry installation.

Wait for indexed knowledge

After ingestion accepts a document, wait for a fresh indexed version before retrieving from it:

ready = await q.knowledge.wait_until_ready(
    document_id,
    minimum_document_version=2,
    timeout=120,
    poll_interval=1,
)
const ready = await q.knowledge.waitUntilReady(documentId, {
  minimumDocumentVersion: 2,
  timeoutMs: 120_000,
  pollIntervalMs: 1_000,
  signal: controller.signal,
});

Use the document version returned by your ingestion/update operation, or omit the minimum to accept any fresh ready version. A ready state with fresh: false continues polling. Failed/deleted ingestion raises IngestionFailedError, with the last ingestion receipt in snapshot. Waiting does not upload, reindex or retry failed ingestion. An unknown ingestion state remains pending until the deadline; an unrecognised future state or mismatched document ID fails validation.

Observe a run

Persist your operation's idempotency key before starting a run. Once q.agents.runs.start(...) returns, save its receipt before waiting. The receipt contains agent_id, run_id and stream_key; it contains no API key. Treat stream keys and run results as sensitive application data and keep them in your backend's authorised storage.

# receipt was durably saved after runs.start().
outcome = await q.agents.runs.recover(receipt, timeout=120, poll_interval=1)
if outcome["kind"] == "completed":
    result = outcome["run"]["result_json"]
elif outcome["kind"] == "requires_action":
    # Show the pending approval/input to an authorised user.
    pending_status = outcome["run"]["status"]
// receipt was durably saved after runs.start().
const outcome = await q.agents.runs.recover(receipt, {
  timeoutMs: 120_000,
  pollIntervalMs: 1_000,
  signal: controller.signal,
});
if (outcome.kind === 'completed') {
  const result = outcome.run.result_json;
} else if (outcome.kind === 'requires_action') {
  // Show the pending approval/input to an authorised user.
  const pendingStatus = outcome.run.status;
}

If you already have both IDs, call q.agents.runs.wait_for_completion(agent_id, run_id, ...) or q.agents.runs.waitForCompletion(agentId, runId, options) directly. Recovery does the same observation from a saved receipt. Use the same environment and tenant credentials that created the run; a receipt is not an authentication credential and does not select an API host.

Outcome kindMeaningNext action
completedExecution completedInspect result_json and result_access_error; completion does not guarantee access to every result
failedExecution failedInspect the run error; do not assume a replacement is safe
cancelledServer confirms cancellationInspect retained results and side-effect receipts
expiredExecution deadline expiredReconcile completed side effects before any new submission
requires_actionawaiting_input or awaiting_approvalObtain an explicit decision; use the existing input/approval APIs, then observe again
unknownServer returned an unfamiliar run statePreserve the snapshot and investigate; never treat it as success or auto-resubmit

RunStatus, RunReceipt, RunRecord and RunOutcome are exported by both SDKs. Python run records remain ordinary dictionaries with type hints; TypeScript results expose typed fields and retain unknown extension fields. Existing high-level AgentRun.wait_for_completion() / waitForCompletion() methods now use bounded polling and retain their run-record return shape. Inspect that record's status: they can return when intervention is required as well as when execution ends.

Deadlines, cancellation and persistence

Waits default to 60 seconds, with one second between completed polls. Python durations are seconds; TypeScript durations are milliseconds. The overall deadline covers requests, transport retries, polling delays and progress callbacks. There are no overlapping polls. Nonpositive or nonfinite durations fail before issuing a request.

  • On deadline, catch WorkflowTimeoutError; inspect last_snapshot in Python or lastSnapshot in TypeScript. The last snapshot is absent if no read completed.
  • To stop observing, cancel the Python task or abort the TypeScript AbortSignal. This does not cancel the remote run. Use the existing explicit runs.cancel API when you intend remote cancellation, then observe its actual status.
  • HTTP errors, authentication failures, expired retention, transport failures and callback exceptions propagate. The helper does not add mutation retries. The root client's existing read-retry policy still applies within the overall deadline.
  • Pass an awaited on_update(snapshot) Python callback or onUpdate(snapshot) TypeScript callback to persist progress in your own store. Avoid logging result bodies. Use idempotent writes because repeated snapshots are possible. Callbacks should be bounded; a TypeScript callback already running cannot be forcibly undone when the wait is aborted.
  • Polling snapshots do not replace the event journal. For event-based consumers, persist event.cursor after applying each event, then use runs.stream with last_event_id / lastEventId. See streaming.

If submission failed before you received a valid receipt, there is nothing for recover to identify. Retain the original idempotency key and request body, follow retry safety, and reconcile with the server. An invalid legacy stream receipt raises WorkflowProtocolError rather than returning missing IDs. Do not start a replacement with a fresh key to resolve an uncertain outcome.

On this page