Artifacts and webhook completion
Verify artifact integrity and distinguish execution completion from webhook delivery.
Q3.5 applies to managed agent runs (lifecycle_version=2) and the public artifact API. See run lifecycle, event replay, permissions and SDK support. These changes and SDK 1.1.0 remain locally qualified until deployed/published together.
Verify an artifact
A ready artifact has an id, immutable version, mime_type, checksum_sha256, byte_size and download_url. Read metadata with GET /v1/artifacts/{id}?version=N&include_body=false; download the same version with GET /v1/artifacts/{id}/download?version=N. The relative download URL requires the same authenticated tenant/owner context and current source permissions. It is not a bearer link and contains no server filesystem path.
The server validates decoded bytes before committing readiness, and checks integrity again on download. Downloads have Content-Disposition: attachment, Content-Length, X-Content-Type-Options: nosniff, a SHA-256 ETag and private/no-store caching. Compare the decoded response bytes with the receipt; never hash a JSON or base64 representation instead. Python and TypeScript helpers perform this check:
receipt = await q.artifacts.get(artifact_id, version=2, include_body=False)
content = await q.artifacts.download(artifact_id, version=receipt["version"])const content = await q.artifacts.download(artifactId, 2);A managed successful run includes result_json.artifacts with a final Markdown report and, when collected, a ZIP delivery bundle. Invalid output/finalisation yields an explicit failure rather than successful completion. Image provider URLs are materialised through the outbound policy; missing images are errors, not placeholders. Readiness proves stored, structurally validated bytes; it does not independently certify factual accuracy or the business value of a document.
Limits: 16 MiB of decoded content per version, 128 versions per artifact, 256 KiB version metadata; ZIP validation bounds expanded content and entry count. Missing versions return 404; building/error versions return artifact_not_ready; legacy versions lacking a receipt are listed as unverified and fetch/download returns artifact_unverified. Corruption returns artifact_integrity_failed; permission changes or changed source revisions prevent access. Regenerate legacy artifacts rather than trusting old cache entries or local paths. PostgreSQL failure returns a service error and never switches to cache-only success.
Receive terminal events
Create a subscription with POST /v1/webhooks, an HTTPS url and selected events. Managed terminal events are agent.run.completed, agent.run.failed, agent.run.cancelled and agent.run.expired. Their delivery intent commits atomically with the terminal run transition. The payload contains event, stable event_id, timestamp, created_at, tenant_id and data containing run_id, agent_id, status and an authenticated status_url. Fetch current results through that URL; terminal webhooks do not contain private answer bodies or artifact bytes.
Verify X-Q-Signature: sha256=... using the subscription secret over the exact raw HTTP body, then atomically record the event ID with your own processing effect. X-Q-Event-Id and X-Q-Delivery-Id aid correlation. Return 2xx once accepted. Retries preserve the event ID, original payload, destination and secret. Secret/URL edits affect future deliveries; retain old signing keys until accepted events finish. Deactivating a subscription pauses pending deliveries; deleting it removes its delivery records. An already dispatched request cannot be recalled.
Delivery is at least once. If the receiver accepts a request but its acknowledgement is lost, Q can send the same event again. Receiver deduplication gives one logical effect. A retry or replay never starts another run, calls a provider or adds a usage charge.
Inspect and recover delivery
GET /v1/webhooks/{id}/deliveries returns the latest 50 rows with durable delivery id, event identity, status, attempt_count, next_retry_at, lease_until, last_error, delivered_at, replay_count and delivery_version. pending includes scheduled or leased attempts; delivered means Q durably recorded a 2xx acknowledgement; failed means attempts are exhausted. Safe errors omit receiver response bodies, URLs and secrets.
Workers claim one attempt exclusively, use a 60-second lease and a 15-second total outbound bound, and retry exponentially (2 seconds initially, capped at one hour) for up to 12 attempts. Process death leaves the lease reclaimable. Death on the final attempt becomes webhook_acknowledgement_unknown, because the remote effect may already have occurred.
After fixing the receiver, call POST /v1/webhooks/{id}/deliveries/{delivery_id}/replay. It requeues an exhausted delivery for 12 further attempts with its original ID and payload. Duplicate replay calls on a pending/delivered row are no-ops. Use q.webhooks.deliveries(id) and q.webhooks.replay(id, delivery_id) in Python, or the equivalent TypeScript helpers. Historical delivery_version=1 logs cannot be replayed through this API; they lack a qualified durable intent.
Rollout and boundaries
Apply migration 20261008_030000_completion_delivery before coordinated API/worker rollout. Execution roles need SELECT/INSERT/UPDATE access to the artifact/subscription/delivery tables as appropriate, with mandatory q.tenant_id binding and forced RLS. The worker runs delivery processing separately from lifecycle reconciliation; configure every served tenant in DURABLE_AGENT_RECONCILE_TENANTS. Monitor pending age and failures and scale workers using the shared database claims. Without a configured worker, intents remain safely pending.
Downgrade refuses retained new receipts. Back up data and qualify migration/worker permissions, retention, queue latency and real HTTPS receivers in staging before promotion. Retention/object storage and operational service objectives remain later programme work.
The atomic guarantee covers managed terminal transitions. Legacy answer/deep-research hooks still have a best-effort window before enqueue; once enqueued into version-2 delivery rows, the same retry machinery applies. Legacy low-level outbox replay now updates its original row, retains newly recorded signing secrets and runs asynchronously; historical entries cannot recover a signing secret that was never stored. Do not treat legacy scheduling as a durable workflow acceptance receipt.