Overview
The Workflows API executes reusable processes you have saved in Martini. The API starts runs with your material, reports their progress, and hands back the output files. It does not author or edit workflows; do that on a canvas in Martini, or through the MCP connector.
The same API key serves the Generation API. Every workflow route lives under https://api.martini.film/v1.
Workflows and runs
A workflow is a reusable production task saved in your workspace: its steps, the variables it reads, and the bins it expects material in. A run is one complete execution—like rolling the camera. Every run starts from a fresh copy of the workflow with the material you pass; running it again with the same material can produce a new variation without requiring any change to the workflow.
One workflow can have many runs. Each run has its own inputs, canvas, progress, spend, and outputs.
More examples
| Workflow | Runs |
|---|---|
| Create establishing shot | Harbor at dawn A; Harbor at dawn B; Mountain lodge in heavy rain |
| Animate storyboard frame | Kitchen reveal, slow push; Rooftop escape, handheld |
| Create prop concept | Hero sword, weathered A; Hero sword, weathered B; Evidence recorder, damaged |
| Storyboard a scene beat | Sophie finds the letter, classic coverage; Gabriel enters the bar, single take |
workflowId chooses the workflow, the request body carries the material (bins) and text (variables), and the response's id identifies that roll. Use the run id to poll progress and fetch results. Runs land in your workspace's Workflow runs project in Martini, one canvas each; openInMartini on the run links straight to it.Quick start
To try the calls without writing code, open the playground: paste a key, pick a workflow, start a run, and read the curl for each request.
1. Create an API key
An organization admin creates keys under Settings → Martini API for a personal workspace, or the team's Martini API tab. The full key is shown once. Store it in a server-side secret such as MARTINI_API_KEY; never expose it in browser code or a NEXT_PUBLIC_* variable.
2. Find the workflow and upload your material
On the Workflows page, open the workflow and use the Connect dialog to copy the curl below with its id and input names filled in. Upload the files its input bins expect with the asset endpoints (POST /assets/uploads/…, or assets.upload() in the client); they land in your API Generations project and their assetIds are what a run takes.
3. Check the connection
4. Start a run
The response is HTTP 201 with the run object. Pass asset ids for every input bin the workflow lists (keyed by bin name or id) and, optionally, values for its variables.
5. Poll, then fetch the results
Poll status until it is completed, failed, or cancelled. Results answers at any time with the takes that have settled so far, so a dashboard can show outputs as they land; status in its body says whether more may come.
Or with a client
Authentication
Send the key as Authorization: Bearer <key>. The Generation API's Authorization: Key <key> form is accepted too, so one client can serve both. Requests belong to the key's organization; anything outside it reads as 404, never 403, so ids from another workspace cannot be probed.
An optional X-Martini-API-Version: 2026-08-13 header pins the response envelope. The served version is echoed back on every response. Runs act as the key's creator: for a placed workflow, or a projectId you name, that user must be able to edit the project, and the project's organization must have workflows enabled.
Endpoints
GET /v1/me
Connection check. Returns the organization and key label the request authenticated as, plus the API version in effect.
GET /v1/projects
The organization's projects the key's user can see — the same set the MCP connector's get_projects shows them — most recently joined first. Use it to pick the projectId a run, a generation, or an upload should land in. A project the user can only view is listed with canEdit: false rather than hidden; naming it for a run answers PROJECT_WRITE_FORBIDDEN.
| Parameter | Type | Notes |
|---|---|---|
query | string | Optional. A project name or id, or a fragment of either (1–200 characters). Matches are ranked: an exact id or name first, then a name that starts with, contains, or holds every word of the query, then an id fragment. |
exactName | boolean | Optional, with query. true keeps only projects whose name matches it exactly (case-insensitive), or whose id is it. |
limit | integer | Optional page size, 1–50 (default 20). totalMatched counts every match; truncated says whether more matched than were returned. |
visibility is what a share link grants someone who is not a member: private, link_view, or link_edit. openInMartini opens the project on its default canvas. A parameter out of range answers 400 INVALID_REQUEST.
GET /v1/projects/{projectId}/canvases
A project's canvases in the order the app lists them. The first carries isDefault: true: it is where the project opens, and where a generation into the project lands when no canvasId is given. The project must be the organization's and readable by the key's user; anything else answers 404 PROJECT_NOT_FOUND, so a canvas name never crosses a project boundary.
GET /v1/workflows
The saved workflows in the key's workspace: steps, inputs (variables with their saved default values, and the input bins a run must fill), outputs, and the fingerprint of the machinery. Add ?all=true to append the placed workflows the key's user can run in place, each with its project, canvas, and the canvas state as it stands.
A placed workflow in the same listing (with ?all=true):
GET /v1/workflows/{workflowId}
One workflow of either kind in the same shape as the listing, or 404 WORKFLOW_NOT_FOUND when it is not in the key's workspace, is a copy stamped for a run, or lives in a project the key's user cannot edit.
POST /v1/workflows/{workflowId}/runs
Start a run. For a saved workflow, Martini stamps a fresh copy onto its own canvas in your workspace's Workflow runs project (or in the projectId you pass), fills its input bins with the assets you pass, and runs it. For a placed workflow, the run happens in place and reads the canvas bins you do not pin.
| Field | Type | Notes |
|---|---|---|
bins | object | Asset ids per input bin, keyed by bin name (case-insensitive) or id. Required for every input bin of a saved workflow. See Inputs. |
inputs | object | JSON documents for the input bins that declare a fileInput, keyed by its key: { json: document } sends the document, { fileId } reuses a JSON File in the project, an array supplies several. See Inputs. |
variables | object | Optional. Values for the variables the workflow reads, keyed by variable name (case-insensitive) or id. See Inputs. |
fingerprint | string | Optional. The fingerprint you last read for this workflow; a mismatch answers 409 WORKFLOW_CHANGED. See Fingerprint. |
oliveBudget | number | Optional cap on generation spend for the run, in olives. See Budget. |
idempotencyKey | string | Optional. Re-posting the same key returns the run the first call created instead of starting another (and stamps no second copy). |
projectId | uuid | Saved workflows only. Optional. Run in this project of your workspace, on a new canvas, so the outputs land where your team works. A run always gets a canvas of its own. |
Responds 201 with the run object and links.status / links.results. The run starts immediately.
GET /v1/runs/{runId}
The run object, with per-Action and per-item progress and the live phase line. Add ?include=activity to get each Action's events (its activity trail) as well; leave it off for a plain poll.
GET /v1/runs/{runId}/status
Compact live activity: the last activity time, executor, model steps, output counts, and olives. Plans, inputs, and traces are omitted. Progress describes active work only while the run is running. Accepted counts takes that passed review; planned counts cover plans produced so far, not every shot required by a brief. Older runs may have no activity record.
GET /v1/runs/{runId}/results
Answers at any time with the takes that have settled so far; status says whether more may come. Call it when a status poll shows a new completed item, or once the run settles, rather than on every poll: every call presigns every output.
Only items that completed with an output appear; group them by actionId to read one step's outputs. Each url is a presigned download valid for expiresIn seconds; fetch it promptly rather than storing it. A failed run returns its partial outputs with the failure copy in error. An output whose file cannot be resolved comes back with status: "not_ready" or "missing" and an error instead of failing the whole response. Files from an organization on a free plan carry the same watermark they would through the app.
POST /v1/runs/{runId}/resume
Retry the stopped Action of a failed or needs_revision run. Finished Actions and finished units keep their outputs, and nothing already spent is charged again. Answers 200 with the run, pending again; poll it as before. Any other status answers 409 WORKFLOW_RUN_NOT_RESUMABLE. A needs_revision run re-reads the canvas inputs it names, so a fix on the canvas lands on resume; inputs pinned on the run (variables, bins) do not change, so such a run resumed unchanged stops the same way — change them and start a new run instead.
POST /v1/workflows
The advanced path: place a copy of a saved workflow on a canvas of your choosing, so a team can prepare material there and run it in place later. { "from": "<savedId>", "projectId": "…", "canvasId"?: "…", "name"?: "…" } answers 201 with the new placed workflow in the listing shape. Most integrations never need this; a run of a saved workflow already gets a copy of its own.
The run object
| Field | Meaning |
|---|---|
id | The run id. Use it for status and results. |
savedWorkflow | The saved workflow the run was started from and its version then; null for an unsaved placed workflow. |
fingerprint | Content identity of the machinery this run executed. |
workflowId, project, canvas | The placed workflow that ran and where it lives. Present when the run executed a placed workflow you can see: a run of a placed workflow, or a run of a saved workflow started with projectId (its copy lands in that project). Absent for runs of saved workflows in the Workflow runs project, whose copies are hidden. |
status | pending, running, completed, failed, or cancelled. API runs never wait for approval. A failed run can be resumed (POST /v1/runs/{runId}/resume). |
phase | What Martini is doing right now, the same line the app shows ("unit 3/7 · waiting on generations"). Display copy: show it, do not parse it. Null unless the run is running. |
origin | Always "api" for runs started here. Runs from the Martini app, the assistant, or MCP carry their own. |
createdAt, startedAt, completedAt | ISO timestamps; startedAt and completedAt are null until reached. |
error | Copy written for a human when the run failed; otherwise null. |
variables | The values pinned for this run, keyed by variable name; empty when the canvas values were used. |
bins | The asset ids pinned for each input bin, keyed by bin name; empty when the canvas bins were read. |
olives.budget | The spend rail in olives, or null before it is armed. |
olives.budgetSource | "explicit" when you set oliveBudget; "auto" when Martini armed it from the estimate. |
olives.generation, olives.agent | Olives spent so far on generation and on the agent. |
actions[] | One row per Action in run order: id, name, status, outcome, error, startedAt, completedAt, plan (units, outputsPerUnit, estimatedOlives, summary; null before the plan pass), and olives (generation summed from its takes, agent measured on the step). |
actions[].events | Only with ?include=activity: the step’s activity trail, the feed the app’s inspector shows. Each event has elapsedMs, kind (plan, tool, unit, revision, settle, judge, sweep, note), and optionally tool, unit (1-based), take, durationMs, note, detail, outputAssetIds. A finished step’s events never change. |
items[] | One row per output take: id, actionId, status, outcome, outputAssetId, error, revision, createdAt, completedAt, verdict (the judge’s pass, reason, take; null without a judge), replacesItemId. A take appears once its outputs have settled. Status is pending, running, completed, failed, needs_revision (the agent stopped before generating because the inputs fail a precondition the brief sets; revision carries its reason), rejected (a judged take that did not pass), superseded, rerunning, or cancelled. |
outcome | The settled result beside status, on every action and item: null until settled, then "generated", "failed", or "needs_revision" — the same word as status, kept for callers that read it. needs_revision means the agent stopped before generating because the inputs fail a precondition the brief sets; the action and the run carry status "needs_revision" too. Change the inputs, then resume the run or start a new one. |
items[].revision | Set when status is "needs_revision": the agent’s full reason and the inputRefs it named (ids or names from the run’s inputs; empty when none, and always empty when the agent only answered with the DECLINED: marker). null on every other item. |
openInMartini | The run’s canvas in the Martini app. |
links.status, links.results | Paths of the status and results endpoints for this run. |
Inputs
A workflow's inputs are what its briefs reference: canvas variables (a script, a character description, a product name) and the bins it reads that nothing in the chain writes (reference frames, source clips). GET /v1/workflows lists them: a saved workflow from its definition (variables with their saved defaults, bins by name), a placed workflow with the canvas state as it stands.
bins carries the material: keys are bin names (case-insensitive) or ids, values are asset ids in the order the Action should read them. A run of a saved workflow starts from empty bins, so every input bin must be pinned (WORKFLOW_BIN_REQUIRED otherwise). Assets must be completed and in a project the key's user can read: your uploads (POST /assets/uploads/…, which land in the API Generations project), the projectId you passed, or an earlier run's output in Workflow runs. Each is copied into the run's canvas as a new record on the same storage objects. A placed workflow reads the canvas bins you do not pin; pinned assets must be in its project.
An input bin can declare a fileInput (shown on the bin in GET /v1/workflows: its key, the format json, and whether it takes one file or many). Fill it with inputs instead of bins: keys are the declared keys, values are { "json": document } to send the document itself (up to 10 MB, arbitrary JSON; embedded images travel as-is), { "fileId": "…" } to reuse a JSON File already in the project, or an array of those for a many-file input. Martini stores each document as an immutable revision, freezes it into the run, and hands the Action a local copy; the canvas and the run record hold only its descriptor. A saved workflow may carry default documents; a run that names none uses them, and a required input with no default answers INVALID_FILE_INPUT.
variables pins a value for any variable for this run only. Keys are variable names (case-insensitive, as shown on the variable card) or ids; values are strings and replace the whole value. Unpinned variables keep their saved (or canvas) value. Only inputs the workflow's Actions read are accepted; any other key answers 400 WORKFLOW_VARIABLE_NOT_FOUND or WORKFLOW_BIN_NOT_FOUND with the accepted inputs listed under inputs. The run echoes what it pinned under variables and bins on every status read. Limits: 20,000 characters per variable and 100,000 in total; 20 bins with 200 assets each.
Fingerprint
Every workflow and run carries a fingerprint: a hash of the machinery (each Action's brief, references, output bins, agent, pinned settings, and gate, plus the wired bins), never of names, bin contents, or variable values. Ids stay stable when a workflow is edited; the fingerprint changes. Pass the one you read to POST /v1/workflows/{workflowId}/runs and a changed workflow answers 409 WORKFLOW_CHANGED with the current fingerprint instead of running something else. Omit it to run whatever the workflow is now.
Budget
oliveBudget is optional. When omitted, the run records budgetSource: "auto" and Martini arms the spend rail at each Action's gate from that Action's estimate, raising it for a costlier later Action rather than failing. When supplied, a plan estimated over the budget fails the run with the estimate in error; raise the budget and run again. Both generation and agent spend are reported in olives as the run progresses.
Errors
Errors are JSON { "error": "<copy for a human>", "code": "<STABLE_CODE>" }. Branch on code; the copy may change.
| HTTP status | Code | When |
|---|---|---|
401 | INVALID_API_KEY | Missing, malformed, or unknown key. |
401 | API_KEY_REVOKED | The key was revoked. |
401 | API_KEY_EXPIRED | The key passed its expiry; CLI keys expire after 90 days — run martini login again. |
403 | API_KEY_DEFAULTS_MISSING | The key lost its default project. Contact support. |
403 | GENERATION_API_DISABLED | The organization no longer holds API access. |
503 | GENERATION_API_ACCESS_UNAVAILABLE | Access could not be checked. Retry. |
400 | UNSUPPORTED_API_VERSION | Unknown X-Martini-API-Version header. |
400 | INVALID_REQUEST | Body is not JSON, a field does not fit the workflow kind (projectId on a placed workflow), or a query parameter is out of range (GET /v1/projects). |
404 | WORKFLOW_NOT_FOUND | No such workflow in this organization, or a copy you cannot see. |
404 | PROJECT_NOT_FOUND | projectId is not a project of this organization, or the key owner cannot read it (GET /v1/projects/{projectId}/canvases). |
409 | PROJECT_DOCUMENT_TOO_LARGE | The project document is too large to open, so its canvases cannot be listed. Durable: retrying will not help. Contact support. |
503 | PROJECT_DOCUMENT_UNAVAILABLE | The project document could not be read while listing canvases. Retry. |
403 | PROJECT_WRITE_FORBIDDEN | The key owner cannot edit the project. |
403 | WORKFLOWS_NOT_ENABLED | The project organization lacks the workflow grant. |
400 | WORKFLOW_VARIABLE_NOT_FOUND | A variables key names no variable the workflow reads; the body lists what it accepts under inputs. |
400 | WORKFLOW_BIN_NOT_FOUND | A bins key names no input bin of the workflow; the body lists what it accepts under inputs. |
400 | WORKFLOW_BIN_REQUIRED | A saved workflow input bin was not pinned. A run starts from empty bins, so pass asset ids for every input bin. |
400 | WORKFLOW_ASSET_NOT_FOUND | A bins value names an asset the key owner cannot read, or one that is still processing. Upload it first. |
4xx | INVALID_FILE_INPUT | An inputs entry is not valid JSON or exceeds 10 MB (400), names a JSON File the key owner cannot read (404), or reuses an idempotency key with different documents (409). |
409 | WORKFLOW_CHANGED | The workflow changed since the fingerprint you passed; the body carries the current one. |
4xx | WORKFLOW_RUN_REJECTED | The run could not start, for example because the workflow has no Actions. |
409 | WORKFLOW_RUN_NOT_RESUMABLE | Only a failed run can be resumed. |
500 | WORKFLOW_RUN_FAILED | Starting or resuming the run failed on the Martini side. Retry. |
4xx | WORKFLOW_CREATE_REJECTED | POST /v1/workflows was refused, for example because the canvas does not exist. |
500 | WORKFLOW_CREATE_FAILED | Placing the workflow failed on the Martini side. Retry. |
404 | WORKFLOW_RUN_NOT_FOUND | Unknown run, or a run from another organization. |
Using the client
@martini-film/client 0.10.0 and later (JavaScript, TypeScript) and martini-client 0.3.0 and later (Python) wrap every call. Martini-native routes reject with MartiniApiError, which carries status, the stable code, and the parsed body; the Python client raises a MartiniAPIError subclass per status (ConflictError, RateLimitError, …) with the same fields.
For any other language, the complete /v1 surface is described by an OpenAPI 3.1 document at api.martini.film/v1/openapi.json (no authentication) — point a client generator or a coding agent at it.
workflows.subscribe() and runs.wait() poll with jittered backoff from 2 seconds up to 15 seconds and give up after 30 minutes by default; pass pollInterval and timeoutMs (poll_interval and timeout in Python) to change that. Both return a failed or cancelled run rather than throwing; check status. Use Node.js 18 or later or Python 3.10 or later, and call Martini from trusted server code only.
Not yet available
- Webhooks when a run settles. Poll the status endpoint for now.
- Bins as an identity on the wire (what each bin holds so far on the run, the bin name on each result). Group takes by
actionIdfor now. - Authoring or editing a workflow over this API. Build it on a canvas in Martini, or via MCP.
- Notes and directly tagged assets as per-run inputs. Only variables and input bins can be pinned.
- Deleting a run or its canvas. Runs stay in the Workflow runs project until you remove them in the app.