Model an AI generation job as a state machine in your database
A schema and update rule for tracking Sume jobs: five statuses, sticky terminal states, a separate webhook delivery column, and the idempotency key on the row.

Give each Sume job one row with five possible statuses (queued, processing, completed, failed, canceled), make the three terminal ones sticky, and keep webhook delivery in its own column. That is the whole machine. The mistake to avoid is a single status field that mixes the job's outcome with the delivery of its webhook, because the two move independently.
The statuses and what they mean
Sume documents five job statuses. queued is a normal accepted state: workspace concurrency is applied when workers move a job to processing, not when the API accepts it. A 2xx on submit means the job exists and paid work is in flight, not that it finished.
| Field | Values | Notes |
|---|---|---|
Job status | queued, processing, completed, failed, canceled | Terminal: completed, failed, canceled. The terminal and result_ready booleans on the status payload say when to stop and when to fetch. |
| Webhook delivery status | pending, delivering, delivered, retrying, failed, exhausted | Describes the callback, not the job. A job can be completed while its delivery is exhausted. |
| Resource status | processing, ready, failed, canceled, archived | A separate vocabulary for resources such as avatars. Do not mix it with job status. |
The allowed moves
A job moves forward only. It starts queued, can move to processing, and ends in exactly one of completed, failed or canceled. A queued job can also be canceled before generation starts. Cancel after generation starts returns 409 job_generation_already_started with details.cancelable: false, and the job then finishes or fails normally. Cancel on an already-canceled job is idempotent and returns the same canceled job.
Two consequences follow for storage. First, a late or out-of-order read must not overwrite a terminal state, so the write is conditional. Second, GET /v1/jobs/{id}/result is only valid for completed jobs and answers 409 job_not_completed otherwise, so read failures from the job record (GET /v1/jobs/{id}), which carries the public error.
A schema with sticky terminal states
The table keeps the idempotency key next to the job id, so a crashed submitter can look up its own work instead of submitting again. webhook_status is nullable because polling-only integrations never receive one. The update statement is the guard: it only moves a row that is still queued or processing, so a stale poll that reads processing after a webhook already wrote completed changes nothing.
create table gen_job (
job_id text primary key,
idempotency_key text not null unique,
status text not null check (status in ('queued','processing','completed','failed','canceled')),
webhook_status text,
error_code text,
updated_at text not null default current_timestamp
);
-- Terminal states are sticky: a late poll can never move a job backwards.
update gen_job
set status = :new, updated_at = current_timestamp
where job_id = :id
and status in ('queued', 'processing');Webhook and poll write the same way
Both the webhook handler and the poller should call the same function that applies the guarded update. Sume documents the job webhook as a terminal-only event, and recommends keeping the status poll as a backup, so both transports can legitimately race to write the same terminal state. With the sticky update the second writer is a harmless no-op, and the rowcount tells you which writer won if you care about firing side effects exactly once.
Keep the side effects out of the update. Write the status, then enqueue the follow-up work (copy the file, notify the user) keyed by the job id, so a replayed webhook cannot enqueue it twice.
Reading the machine in a UI
Map the five statuses to three things a person sees: working (queued and processing), done (completed), and needs attention (failed and canceled). Do not show queued as a problem. On a workspace with a concurrency limit of 1, several valid jobs can sit in queued while one runs, and each of them moves on its own when a slot opens. Sume shows queue counts and the remaining accepted capacity but no precise queue position or ETA for a single job, so avoid promising one in your interface.
The webhook delivery column belongs in an admin or support view, not the customer view. A job whose delivery is retrying or exhausted is still a finished or running job; the column only tells your team that the push path needs attention and that the poll path must carry the result.
Two columns people forget
Store the error code from a failed job, not just the word failed. Failed jobs carry public error metadata such as category, retryability and next action, and those decide whether your system retries, asks the user to change the input, or stops. Also store the request_id from error bodies. It is safe to share with Sume support and is the first thing they ask for.
Sources
Related posts
More in Developers
- Pin the model id in an ad test: sume/auto follows the catalog
sume/auto is a pure function of the request plus the catalog version, so two ad arms made weeks apart can land on different models. Pin an explicit id in tests.
- Poll hundreds of AI jobs without a thundering herd: jitter and budgets
Poll many Sume jobs without synchronized bursts: jitter, next_poll_after_seconds, per-plan read budgets, and the math on how much polling a plan can absorb.
- Portuguese speech to text API: Sume STT language_code pt or pt-BR
Transcribe Portuguese audio with Sume STT using language_code pt or pt-BR, then check the reported language and word times. $0.01 per audio minute.
- Probe a finished video before upload: duration, size and aspect
Run video inspect with frames false to read a render's duration, size and frame rate before posting. Check it against the 3-minute Shorts limit.
Written by Sume