MediaJobFailedPayload media.job.failed@1
Generated at from
openvibe-contracts v0.33.0 and
openvibe-sdk v0.5.0.
- Version
- 1.0.0
- Owner
- media
- Visibility
- first-party
- Status
- active
- Compatibility
- backward
- Decision
- ADR-004
- Schema
https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json
media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.
Fields
| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
id | string | yes | The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it. |
|
app_id | string | yes | The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…). |
|
object_id | string | null | yes | The object the job works on; null for a tenant-wide job (invariant.scan). |
|
type | string | yes | Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux. |
|
status | const | yes |
| |
attempts | integer | yes | Attempts started, the last failed one included. |
|
max_attempts | integer | yes |
| |
error_code | string | null | yes | Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it. | |
cancel_requested | boolean | yes | Its owner asked a running job to stop. | |
has_result | boolean | yes | The handler left a partial result. The result itself is never in the event: GET the job with a tenant token for it. | |
run_after | null | yes | ||
decided_at | string | null | yes | When its owner approved it, or cancelled it while it was proposed or queued; null otherwise. |
|
created_at | string | yes |
| |
updated_at | string | yes |
| |
started_at | string | yes | The first attempt's start. |
|
finished_at | string | yes |
|
Examples
From the contract's own test fixtures: valid ones validate, rejected ones must fail.
Valid: media-unavailable
{
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"status": "failed",
"attempts": 3,
"max_attempts": 3,
"error_code": "media_unavailable",
"cancel_requested": false,
"has_result": false,
"run_after": null,
"decided_at": null,
"created_at": "2026-09-23T18:02:11.000Z",
"updated_at": "2026-09-23T18:02:13.000Z",
"started_at": "2026-09-23T18:02:12.000Z",
"finished_at": "2026-09-23T18:02:13.000Z"
}Valid: no-code-partial-result
{
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"status": "failed",
"attempts": 3,
"max_attempts": 3,
"error_code": null,
"cancel_requested": false,
"has_result": true,
"run_after": null,
"decided_at": null,
"created_at": "2026-09-23T18:02:11.000Z",
"updated_at": "2026-09-23T18:12:13.000Z",
"started_at": "2026-09-23T18:02:12.000Z",
"finished_at": "2026-09-23T18:12:13.000Z"
}Rejected: carries-error-text
{
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"status": "failed",
"attempts": 3,
"max_attempts": 3,
"error_code": "media_unavailable",
"cancel_requested": false,
"has_result": false,
"run_after": null,
"decided_at": null,
"created_at": "2026-09-23T18:02:11.000Z",
"updated_at": "2026-09-23T18:02:13.000Z",
"started_at": "2026-09-23T18:02:12.000Z",
"finished_at": "2026-09-23T18:02:13.000Z",
"error": "The source bytes are unavailable (no local file and no cloud copy)"
}Rejected: sqlite-finish-time
{
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"status": "failed",
"attempts": 3,
"max_attempts": 3,
"error_code": "media_unavailable",
"cancel_requested": false,
"has_result": false,
"run_after": null,
"decided_at": null,
"created_at": "2026-09-23T18:02:11.000Z",
"updated_at": "2026-09-23T18:02:13.000Z",
"started_at": "2026-09-23T18:02:12.000Z",
"finished_at": "2026-09-23 18:02:13"
}Rejected: unfinished
{
"id": "mjob_01JAB3C4D5E6F7G8H9J0K1MNPQ",
"app_id": "live",
"object_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"type": "object.split",
"status": "failed",
"attempts": 3,
"max_attempts": 3,
"error_code": "media_unavailable",
"cancel_requested": false,
"has_result": false,
"run_after": null,
"decided_at": null,
"created_at": "2026-09-23T18:02:11.000Z",
"updated_at": "2026-09-23T18:02:13.000Z",
"started_at": "2026-09-23T18:02:12.000Z",
"finished_at": null
}Validate
const contracts = require('openvibe-contracts');
contracts.validate('media.job.failed@1', value); // { valid, errors: [{ path, message }] }