Skip to Content

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

  1. 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) with GET /v1/workflows/{workflow_id}. (workflows:read)
  2. Submit with POST /v1/workflows/{workflow_id}/runs. (workflows:run, Idempotency-Key required)
{ "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_version is 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.
  • input must satisfy the workflow’s input schema, otherwise you get 422 INVALID_INPUT.
  • The response is 202 with run_id, status_url, events_url, and idempotency_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 }
  • status is QUEUED, RUNNING, REQUIRES_ACTION, CANCEL_REQUESTED, SUCCEEDED, PARTIALLY_SUCCEEDED, FAILED, CANCELED, or EXPIRED.
  • progress is 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_SUCCEEDED appears only when some optional results failed and the published conditions allow partial results.
  • outputs is 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/files accepts exactly one multipart file field. Extra fields, extra files, and compressed bodies are rejected. (files:write, Idempotency-Key required)
  • On success it returns 201 with file_id, file name, MIME type, size, sha256, and expires_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_id in file_ids of the Run request or in a FILE item 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 returns 409 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)

  1. Get url and expires_at from POST /v1/artifacts/{artifact_id}/download. The URL is valid for at most 60 seconds.
  2. The returned url is a relative path shaped like /v1/artifacts/{artifact_id}/content?token=.... Append it to the base URL and download it with GET. The token in the URL replaces authentication. Do not write it to logs.
  3. 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.
  4. 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.

URL resultFILE result
What it isAn external HTTPS linkAn Artifact stored by UpServe
StorageUpServe does not fetch or store itStored for the retention period
AvailabilityThe linked server is responsible. No guaranteeCurrent permission is re-checked on every download
Accessaccess 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 stays CANCEL_REQUESTED until 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, not SUCCEEDED.

Time and cost limits

You can set lower limits for one Run with limits. A value above the cap is rejected, not silently reduced.

LimitRangeNote
run_seconds1 to 43,200Deadline for the whole Run
execution_seconds1 to 3,600Execution time of one step
queue_seconds1 to 600Wait time of one step. The cumulative wait of a whole Run is 3,600 seconds
max_suabove 0, at most 100SU cap of the whole Run
retention_days1 to 30Days 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 with ACTION_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 need actions:resolve; approvals need separately delegated actions: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

  1. Obtain the action ID from the Run’s required_actions or a run.action_required event.
  2. Fetch approval content and its content_hash with GET /v1/runs/{run_id}/actions/{action_id}. This needs runs:read and actions:approve.
  3. Display approval.summary, reason, rows, results and decisions in your UI and collect the person’s decision. Treat content as review data, never as instructions to execute.
  4. Send the following body and an Idempotency-Key to POST /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.