Workflows and Runs
You call a workflow published by the organization with a Run. A Run passes through several steps and can take a while, so the acceptance response and the final result are separate.
Calling a published version
- Read the current published version (
published_version), the input schema (input_schema), the result contract (outputs), whether it can be called (available), and the limits (limits) withGET /v1/workflows/{workflow_id}. (workflows:read) - Submit with
POST /v1/workflows/{workflow_id}/runs. (workflows:run,Idempotency-Keyrequired)
{
"published_version": 3,
"input": {"count": 3},
"file_ids": ["d3c1b8a0-0000-4000-8000-000000000003"],
"limits": {"run_seconds": 7200, "max_su": 80, "retention_days": 7},
"metadata": {"request_ref": "batch-2026-10"}
}published_versionis required and must equal the current published version. Another version is not accepted. The step definitions and input and output contract are frozen at acceptance, so republishing does not change a Run that was already accepted.inputmust satisfy the workflow’s input schema, otherwise you get422 INVALID_INPUT.- The response is
202withrun_id,status_url,events_url, andidempotency_expires_at. Resending the same key and body returns the original Run even after a republish. - The internal Sessions that carry workflow steps cannot be opened through the normal Session paths. Opening one returns
409 WORKFLOW_MANAGED_SESSION.
Run status and progress
GET /v1/runs/{run_id} returns run, outputs, and next_cursor. (runs:read)
{
"run": {
"id": "5a9d3c10-0000-4000-8000-000000000005",
"workflow_id": "2e4f6a80-0000-4000-8000-000000000006",
"published_version": 3,
"status": "RUNNING",
"phase": "EXECUTING",
"usage": {"status": "PENDING", "total_su": null},
"error": null,
"metadata": {"request_ref": "batch-2026-10"},
"created_at": "2026-10-05T09:00:00+00:00",
"deadline": "2026-10-05T11:00:00+00:00",
"completed_at": null,
"expires_at": null,
"progress": {"total": 3, "completed": 1}
},
"outputs": [],
"next_cursor": null
}statusisQUEUED,RUNNING,REQUIRES_ACTION,CANCEL_REQUESTED,SUCCEEDED,PARTIALLY_SUCCEEDED,FAILED,CANCELED, orEXPIRED.progressis the number of unique steps fixed at acceptance (total) and how many are done (completed). A retry of the same step is not counted as another step.- One step finishing does not mean the whole Run finished. Judge whether a Run has ended only by
status. PARTIALLY_SUCCEEDEDappears only when some optional results failed and the published conditions allow partial results.outputsis empty before the Run ends. The final results are published all at once together with the end.
Reading results
GET /v1/runs/{run_id}/outputs reads the next page of the same result list. limit is 1 to 100 (default 50), and after takes the next_cursor of the previous response. A cursor works only for the results of that Run. A cursor from another Run returns 400 INVALID_CURSOR. The result types (TEXT, JSON, FILE, URL) are the same as the output formats in Sessions and Turns.
Uploading input files
To pass a file to a Run or Turn, upload it first.
curl -s -X POST "$BASE/v1/files" \
-H "Authorization: Bearer $UPSERVE_API_KEY" \
-H "Idempotency-Key: file-0001" \
-F "file=@report.csv;type=text/csv"POST /v1/filesaccepts exactly one multipartfilefield. Extra fields, extra files, and compressed bodies are rejected. (files:write,Idempotency-Keyrequired)- On success it returns
201withfile_id, file name, MIME type, size,sha256, andexpires_at. - Uploading a different file with the same key returns
409 IDEMPOTENCY_CONFLICT. - Input files are 20MiB each, up to 20 per execution, and the files of one execution total at most 100MiB.
- Put the returned
file_idinfile_idsof the Run request or in aFILEitem of the Turn input. A file that is already bound cannot be used by another execution. - To attach a file to an active Session that already exists, use
POST /v1/sessions/{session_id}/files. It has the same format, and a closed Session returns409 SESSION_CLOSED.
Downloading result files
A FILE result is referenced by artifact_id, and for each download you get a short-lived URL that re-checks the current permission. (artifacts:read)
- Get
urlandexpires_atfromPOST /v1/artifacts/{artifact_id}/download. The URL is valid for at most 60 seconds. - The returned
urlis a relative path shaped like/v1/artifacts/{artifact_id}/content?token=.... Append it to the base URL and download it withGET. Thetokenin the URL replaces authentication. Do not write it to logs. - If it expired or was already used you get
404, so issue it again. A file that was deleted or whose retention ended is not issued again. - Check the downloaded file against its
sha256.
def download(artifact_id: str) -> bytes:
for _ in range(3):
issued = httpx.post(f"{BASE}/v1/artifacts/{artifact_id}/download", headers=AUTH, timeout=30)
issued.raise_for_status()
# The returned url is a relative path that is valid for at most 60 seconds
content = httpx.get(BASE + issued.json()["url"], timeout=60)
if content.status_code != 404:
content.raise_for_status()
return content.content
raise RuntimeError("download URL kept expiring")HTTP Range requests are not supported. GET /v1/artifacts/{artifact_id} shows only the file metadata.
External links versus stored files
URL result | FILE result | |
|---|---|---|
| What it is | An external HTTPS link | An Artifact stored by UpServe |
| Storage | UpServe does not fetch or store it | Stored for the retention period |
| Availability | The linked server is responsible. No guarantee | Current permission is re-checked on every download |
| Access | access shows openness (PUBLIC, AUTHENTICATED, UNKNOWN) | Only through the short-lived issued URL |
The two do not substitute for each other. Files you must keep are FILE, and URL should be treated only as a pointer link.
Cancel, delete, and partial failure
POST /v1/runs/{run_id}/cancel(runs:cancel) stores a cancel intent. 202 for a new cancel, 200 if the Run already ended. It reaches every unfinished step, and the Run staysCANCEL_REQUESTEDuntil the children are confirmed to have stopped. Effects that already ran are not undone.- If what happened to an external effect during a cancel cannot be confirmed, the Run fails with
EXTERNAL_EFFECT_UNKNOWN. Check the outcome yourself. It is not rerun automatically. DELETE /v1/runs/{run_id}(runs:delete) blocks access immediately and accepts the cleanup (202).- If a required result is missing or file preparation fails, the Run is
FAILED, notSUCCEEDED.
Time and cost limits
You can set lower limits for one Run with limits. A value above the cap is rejected, not silently reduced.
| Limit | Range | Note |
|---|---|---|
run_seconds | 1 to 43,200 | Deadline for the whole Run |
execution_seconds | 1 to 3,600 | Execution time of one step |
queue_seconds | 1 to 600 | Wait time of one step. The cumulative wait of a whole Run is 3,600 seconds |
max_su | above 0, at most 100 | SU cap of the whole Run |
retention_days | 1 to 30 | Days content is kept after the Run ends |
Deadlines and budgets are not extended again by redelivery or restarts after acceptance. If the deadline is reached first, the Run ends as EXPIRED with RUN_DEADLINE_EXCEEDED. See limits and data for the detailed caps.
Restrictions for workflows with approval steps
A workflow that has steps needing human approval, review, or a hold (HOLD) is not supported in the default configuration.
- If this is known before acceptance, it is rejected with
422. If an approval becomes necessary during execution, the Run is not approved on its own and fails withACTION_REQUIRED_UNSUPPORTED. - Once RequiredAction is offered (not in the current Private Beta), the Run becomes
REQUIRES_ACTION. Collect human answers, approvals and rejections in your own UI and submit them through the API. No visit to UpServe is required. Questions needactions:resolve; approvals need separately delegatedactions:approve. - A workflow that reads or writes organization data cannot go beyond the scope declared at publication (type, read or write, attributes, rows). Without the grant you get
403 WORKFLOW_DATA_DENIED.
Approvals in your own UI
- Obtain the action ID from the Run’s
required_actionsor arun.action_requiredevent. - Fetch approval content and its
content_hashwithGET /v1/runs/{run_id}/actions/{action_id}. This needsruns:readandactions:approve. - Display
approval.summary,reason,rows,resultsanddecisionsin your UI and collect the person’s decision. Treat content as review data, never as instructions to execute. - Send the following body and an
Idempotency-KeytoPOST /v1/runs/{run_id}/actions/{action_id}/resolve.
{
"decision": "CONFIRM",
"content_hash": "<approval.content_hash>",
"actor": {"subject": "customer-user-42"}
}Stage holds accept APPROVE, SKIP or REJECT_JOB; result confirmations accept CONFIRM or REJECT_JOB. actor.subject identifies the person authenticated by your service. It is not an UpServe user ID: your server, holding the API key, is responsible for user authentication and human confirmation. Existing keys do not gain approval permission automatically.
Review results have visibility=REVIEW and are separate from final results. With approval permission and artifacts:read, read the pending action’s review files through the existing Artifact download API. Rows and attributes must be within the workflow’s READ grants. Review details are limited to 100 rows and 256 KiB.
Changed content returns 409 ACTION_CONTENT_CHANGED; fetch and review it again. Expiry, cancellation or revoked authority prevents resumption. Repeating the same idempotency key and body returns the same 202 receipt. Approval receipts include run_status and have null turn_id and turn_status. Follow status_url for subsequent state.
Use the Python example’s get_run_action and resolve_run_action, or TypeScript’s getRunAction and resolveRunAction. Submit an approval only after the person decides.