Asset lifecycle
From queued to ready (or failed) — every state an asset can be in.
Asset lifecycle
Every Motioness asset goes through the same finite-state machine, regardless of mode or model.
textqueued ──→ vision_started ──→ vision_completed │ ▼ stage_started ──→ stage_completed │ (loop mode: stage1 → stage2 → merge) ▼ r2_write_completed │ ▼ ready ──┬──→ webhook_dispatched ──→ webhook_delivered └──→ (cached at edge forever) Any stage can emit *_failed → asset.status = failed → asset.failed event
States
| Status | Meaning |
|---|---|
pending | Generation in flight. Worker may still be in vision / stage1 / stage2 / merge / R2-write. |
ready | mp4 exists in R2, accessible via the proxy URL. Terminal success. |
failed | Pipeline gave up. Inspect error_code in the asset.failed event payload. |
Event types
The full event log is emitted to D1 (asset_events), surfaced via SSE, and dispatched to webhooks. The closed enum:
| Event | When |
|---|---|
queued | Worker accepted the request, asset row inserted |
vision_started | Claude vision call dispatched |
vision_completed | Vision returned a motion prompt |
vision_failed | Vision rejected the source (NSFW, oversize, broken URL) |
stage_started | Replicate prediction submitted (stage ∈ stage1, stage2, merge, single) |
stage_completed | Prediction returned an mp4 |
stage_failed | Prediction errored or timed out |
retry_scheduled | Transient error, retry queued (observational in v1.1) |
r2_write_completed | mp4 stored in R2 |
webhook_dispatched | Outbound delivery enqueued |
webhook_delivered | Customer endpoint responded 2xx |
webhook_failed | Customer endpoint errored or timed out |
webhook_dead_letter | 5 failed attempts, won't retry automatically |
ready | Terminal success — proxy URL is now hot |
failed | Terminal failure — error_code field tells you why |
cancelled | Manual cancel via dashboard |
Each event carries ts, attempt, stage (when applicable), latency_ms (on *_completed), error_code + error_msg (on *_failed), request_id.
Watching events
Three ways to observe asset state:
Browser-friendly, no auth required (uses project key + asset id):
bashcurl -N "https://motioness.com/v1/pk_abc/events/asset_xyz"
Each event arrives as:
textid: 1714801234567event: stage_completeddata: {"stage":"stage1","latency_ms":4820,"asset_id":"a25b…","ts":1714801234567}
Reconnect resumes from Last-Event-ID.
For dashboards. Requires session cookie:
bash# Per-assetcurl -N -b "better-auth.session_token=…" \ "https://motioness.com/api/assets/$ASSET_ID/events"# Per-project (all activity)curl -N -b "better-auth.session_token=…" \ "https://motioness.com/api/projects/$PROJECT_ID/activity"
POST to your endpoint when subscribed events fire. See webhooks guide.
json{ "id": "evt_01HXY…", "type": "asset.completed", "ts": 1714801234567, "asset_id": "a25b…", "project_id": "proj_…", "data": { "mp4_url": "https://motioness.com/r2/assets/…mp4", "bytes": 845321 }}
Stale-pending recovery
If a Replicate webhook never lands (network blip, signature mismatch, etc.), two safety nets kick in:
- Opportunistic reconcile —
GET /api/assets/:idtriggers a Replicate state query if the asset has been pending for >25 seconds. - Cron reconciler — runs every minute, finalizes stuck assets after 5 minutes, marks them
failedwithpipeline_stalledafter 10 minutes.
You don't have to do anything; just don't assume pending is permanent. Poll the asset endpoint or listen on SSE.
Idempotency
Asset IDs are content-addressed, so calling the proxy twice for the same (src, motion, mode, dur, prompt, seed, idem, aspect) returns the same asset_id — no duplicate generation.
To force a distinct asset from the same source, vary idem:
text?idem=variant-a → asset_id A?idem=variant-b → asset_id B
Cancellation
There's no public cancel endpoint. The dashboard exposes a Cancel button on pending assets that emits cancelled and stops billing — generations that have already submitted to Replicate will run to completion but won't count as ready.