The Runs API
Manage App executions on your App's hostname — upload files, create sync/streaming/background runs, list and inspect runs, cancel, send mid-run input, and replay events.
Beyond the single-request invocation in Call your App over HTTP, every App hostname serves a Runs API that gives you full control over runs — a run is one execution of your App. Use it for file uploads, background execution, listing and filtering, cancellation, human-in-the-loop input (answering a run that has paused for a person's decision), and an event stream you can re-attach to at any time.
All endpoints live on your App's hostname and use the same authentication as the App itself — Authorization: Bearer $DYNAMIQ_ACCESS_KEY (Access Key) for private Apps; public Apps need no header:
https://<your-app-hostname>/v1/...Authentication failures are uniform across all endpoints: a missing or invalid key returns 401; a valid key scoped to a different project or organization returns 403; an unknown (or deleted) App or run_id returns 404. Errors are JSON of the form {"error": {"code": "...", "message": "...", "details": ...}}.
| Method | Path | Purpose |
|---|---|---|
POST | /v1/files | Upload a file for later runs |
POST | /v1/runs | Create a run (sync, streaming, or background) |
GET | /v1/runs | List runs |
GET | /v1/runs/{run_id} | Get one run |
POST | /v1/runs/{run_id}/cancel | Cancel a run |
POST | /v1/runs/{run_id}/input | Send human-in-the-loop input to a run |
GET | /v1/runs/{run_id}/events | List a run's stored events |
GET | /v1/runs/{run_id}/stream | Stream a run's events over Server-Sent Events (SSE) |
Upload a file — POST /v1/files
Send multipart/form-data with a single file part (uploads are parsed with a 128 MB memory limit); a missing file part returns 400. The returned id can be referenced by later runs via file_ids. Files are scoped to the App that received the upload — an id uploaded to one App cannot be used in another App's runs.
curl -X POST "https://<your-app-hostname>/v1/files" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
-F "file=@report.pdf"import os
import requests
endpoint = "https://<your-app-hostname>"
headers = {"Authorization": f"Bearer {os.getenv('DYNAMIQ_ACCESS_KEY')}"}
with open("report.pdf", "rb") as f:
response = requests.post(f"{endpoint}/v1/files", headers=headers,
files={"file": f})
file = response.json()["data"]
print(file["id"], file["name"], file["size"], file["mime_type"])const endpoint = "https://<your-app-hostname>";
const headers = { Authorization: `Bearer ${process.env.DYNAMIQ_ACCESS_KEY}` };
import { readFile } from "node:fs/promises";
const form = new FormData();
form.append("file", new Blob([await readFile("report.pdf")]), "report.pdf");
const response = await fetch(`${endpoint}/v1/files`, {
method: "POST",
headers,
body: form,
});
const { data: file } = await response.json();
console.log(file.id, file.name, file.size, file.mime_type);Returns 201 with the file metadata:
{
"data": {
"id": "9c2f8a3e-6f1b-4c0e-9a51-2f4f3a9d8b10",
"name": "report.pdf",
"size": 482133,
"mime_type": "application/pdf"
}
}Create a run — POST /v1/runs
inputobjectrequiredstreambooleanbackgroundbooleanfile_idsstring[]user_idstringsession_idstring (UUID)stream and background are mutually exclusive — setting both returns 400.
The endpoint also accepts multipart/form-data (128 MB parse limit) for inline file upload: form fields input (JSON string), stream, background, file_ids (JSON array string), user_id, session_id, plus one or more file parts named files. Inline files are uploaded and attached to the run automatically.
Mode 1 — synchronous (default)
With neither flag set, the call blocks until the run reaches a terminal status (completed, failed, or canceled) and returns 200 with the run record, including output (or error). The HTTP connection stays open for the whole run — for long-running workflows use streaming or background mode instead, and set generous client timeouts (see Streaming and async):
curl -X POST "https://<your-app-hostname>/v1/runs" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"input": {"question": "What can you do?"}}'import os
import requests
endpoint = "https://<your-app-hostname>"
headers = {"Authorization": f"Bearer {os.getenv('DYNAMIQ_ACCESS_KEY')}"}
response = requests.post(f"{endpoint}/v1/runs", headers=headers,
json={"input": {"question": "What can you do?"}})
run = response.json()["data"]
print(run["status"], run["output"])const endpoint = "https://<your-app-hostname>";
const headers = {
Authorization: `Bearer ${process.env.DYNAMIQ_ACCESS_KEY}`,
"Content-Type": "application/json",
};
const response = await fetch(`${endpoint}/v1/runs`, {
method: "POST",
headers,
body: JSON.stringify({ input: { question: "What can you do?" } }),
});
const { data: run } = await response.json();
console.log(run.status, run.output);{
"data": {
"id": "0f6a2d4e-8b3c-4f1a-9d2e-7c5b6a4f3e21",
"app_id": "5a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"status": "completed",
"input": {"question": "What can you do?"},
"output": {"output": "I can answer questions about..."},
"started_at": "2026-06-10T09:30:00Z",
"ended_at": "2026-06-10T09:30:04Z"
}
}Mode 2 — streaming
With "stream": true the response is an SSE stream of run events, starting at sequence 1 and ending with a terminal event (run.completed, run.failed, or run.canceled). The samples below mirror the Streaming Run with Human Feedback snippet from the Integration tab, including how to answer a human-in-the-loop pause:
curl -N -X POST "https://<your-app-hostname>/v1/runs" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"input": {"question": "What can you do?"}, "stream": true}'import asyncio
import json
import os
import httpx
endpoint = "https://<your-app-hostname>"
token = os.getenv("DYNAMIQ_ACCESS_KEY") # Generate Access Key in the UI settings
headers = {"Authorization": f"Bearer {token}"}
payload = {
"input": {
"question": "What can you do?"
},
"stream": True,
}
run_id: str | None = None
hitl_request_id: str | None = None
hitl_ready = asyncio.Event()
async def stream_events(client: httpx.AsyncClient) -> None:
global run_id, hitl_request_id
async with client.stream(
"POST",
f"{endpoint}/v1/runs",
json=payload,
headers=headers,
timeout=None,
) as response:
async for raw in response.aiter_lines():
if not raw.startswith("data: "):
continue
event = json.loads(raw.removeprefix("data: "))
print(event)
if event["type"] == "run.created":
run_id = event["data"]["id"]
elif event["type"] == "agent.human_feedback.requested":
hitl_request_id = event["data"]["id"]
hitl_ready.set()
async def main() -> None:
async with httpx.AsyncClient() as client:
stream_task = asyncio.create_task(stream_events(client))
# Optional: answer a human-feedback pause while the stream runs.
# await hitl_ready.wait()
# await client.post(
# f"{endpoint}/v1/runs/{run_id}/input",
# json={"type": "human_feedback", "data": {"request_id": hitl_request_id, "feedback": "cancel"}},
# headers=headers,
# )
await stream_task
asyncio.run(main())const endpoint = "https://<your-app-hostname>";
const token = process.env.DYNAMIQ_ACCESS_KEY; // Access key should be securely stored
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
const payload = {
"input": {
"question": "What can you do?"
},
"stream": true
};
let runId: string | null = null;
let hitlRequestId: string | null = null;
async function streamEvents() {
const response = await fetch(`${endpoint}/v1/runs`, {
method: "POST",
headers,
body: JSON.stringify(payload),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let idx: number;
while ((idx = buffer.indexOf("\n")) !== -1) {
const raw = buffer.slice(0, idx).trim();
buffer = buffer.slice(idx + 1);
if (!raw.startsWith("data: ")) continue;
const event = JSON.parse(raw.slice("data: ".length));
console.log(event);
if (event.type === "run.created") runId = event.data.id;
else if (event.type === "agent.human_feedback.requested") {
hitlRequestId = event.data.id;
// Answer it: POST `${endpoint}/v1/runs/${runId}/input` with
// { type: "human_feedback", data: { request_id: hitlRequestId, feedback: "..." } }
}
}
}
}
await streamEvents();Mode 3 — background
With "background": true the call returns 202 Accepted immediately with the run record (status created). Fetch the result later with GET /v1/runs/{run_id}, or attach to its live stream with GET /v1/runs/{run_id}/stream.
curl -X POST "https://<your-app-hostname>/v1/runs" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"input": {"question": "What can you do?"}, "background": true}'List runs — GET /v1/runs
statusstring (repeatable)user_idstringsession_idstring (UUID)pageintegerpage_sizeintegerResults are sorted by started_at descending by default. The response contains data (the runs) plus a pagination object with page, page_size, page_count, and total_count.
curl "https://<your-app-hostname>/v1/runs?status=awaiting_input&user_id=user-42" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY"import os
import requests
endpoint = "https://<your-app-hostname>"
headers = {"Authorization": f"Bearer {os.getenv('DYNAMIQ_ACCESS_KEY')}"}
response = requests.get(f"{endpoint}/v1/runs", headers=headers,
params={"status": "awaiting_input", "user_id": "user-42"})
body = response.json()
for run in body["data"]:
print(run["id"], run["status"], run["started_at"])const endpoint = "https://<your-app-hostname>";
const headers = { Authorization: `Bearer ${process.env.DYNAMIQ_ACCESS_KEY}` };
const params = new URLSearchParams({ status: "awaiting_input", user_id: "user-42" });
const response = await fetch(`${endpoint}/v1/runs?${params}`, { headers });
const body = await response.json();
for (const run of body.data) console.log(run.id, run.status, run.started_at);Get a run — GET /v1/runs/{run_id}
Returns the run record. A run blocked on human input reports "status": "awaiting_input" and includes input_requests describing what it is waiting for:
{
"data": {
"id": "0f6a2d4e-8b3c-4f1a-9d2e-7c5b6a4f3e21",
"status": "awaiting_input",
"started_at": "2026-06-10T09:30:00Z",
"input": {"question": "Refund order 1042"},
"input_requests": [
{
"id": "7e1d9b2a-3c4f-4a5b-8d6e-9f0a1b2c3d4e",
"type": "approval_request",
"prompt": "Approve refund of $120 to customer 88?",
"params": {"amount": 120},
"editable_params": ["amount"]
}
]
}
}input_requests[].type is human_feedback (free-text reply expected) or approval_request (confirm/reject, optionally editing editable_params).
Cancel a run — POST /v1/runs/{run_id}/cancel
Marks the run canceled and signals the runtime to stop. Canceling a run that is already completed, failed, or canceled has no effect; both cases return 200.
curl -X POST "https://<your-app-hostname>/v1/runs/$RUN_ID/cancel" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY"Send mid-run input — POST /v1/runs/{run_id}/input
Answers a pending human-in-the-loop request. The run must be in started or paused status, otherwise the call fails with 400. If the run is paused, sending input resumes it automatically from its latest checkpoint (the saved state it can restart from); a paused run with no stored checkpoint returns 400 ("No checkpoint available to resume from").
The body is {"type": ..., "data": ...} where data depends on type:
type | data fields |
|---|---|
human_feedback | request_id (UUID, required), feedback (string, required) |
approval_request.confirmed | request_id (UUID, required), data (object — edited params, optional) |
approval_request.rejected | request_id (UUID, required), feedback (string, required) |
request_id is the id from the agent.human_feedback.requested / approval_request.created event (or from input_requests on the run). An unknown request_id returns 404.
curl -X POST "https://<your-app-hostname>/v1/runs/$RUN_ID/input" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "approval_request.confirmed",
"data": {
"request_id": "7e1d9b2a-3c4f-4a5b-8d6e-9f0a1b2c3d4e",
"data": {"amount": 100}
}
}'import os
import requests
endpoint = "https://<your-app-hostname>"
headers = {"Authorization": f"Bearer {os.getenv('DYNAMIQ_ACCESS_KEY')}"}
run_id = "0f6a2d4e-8b3c-4f1a-9d2e-7c5b6a4f3e21"
hitl_request_id = "7e1d9b2a-3c4f-4a5b-8d6e-9f0a1b2c3d4e"
requests.post(
f"{endpoint}/v1/runs/{run_id}/input",
headers=headers,
json={
"type": "human_feedback",
"data": {"request_id": hitl_request_id, "feedback": "Looks good, proceed."},
},
).raise_for_status()const endpoint = "https://<your-app-hostname>";
const headers = {
Authorization: `Bearer ${process.env.DYNAMIQ_ACCESS_KEY}`,
"Content-Type": "application/json",
};
const runId = "0f6a2d4e-8b3c-4f1a-9d2e-7c5b6a4f3e21";
const hitlRequestId = "7e1d9b2a-3c4f-4a5b-8d6e-9f0a1b2c3d4e";
await fetch(`${endpoint}/v1/runs/${runId}/input`, {
method: "POST",
headers,
body: JSON.stringify({
type: "human_feedback",
data: { request_id: hitlRequestId, feedback: "Looks good, proceed." },
}),
});See Human in the loop for designing workflows that pause for input.
List run events — GET /v1/runs/{run_id}/events
Returns the run's stored events as a paginated list, sorted by sequence ascending by default. Use it to rebuild a transcript after the fact — including for finished runs, where the live stream is no longer useful. It accepts the same page / page_size query parameters as GET /v1/runs (default 25 per page, maximum 500), so fetch subsequent pages until pagination.page_count is reached.
curl "https://<your-app-hostname>/v1/runs/$RUN_ID/events" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY"Stream run events — GET /v1/runs/{run_id}/stream
Attaches to a run's live SSE stream — including runs started with "background": true, or re-attaching after a dropped connection. Pass after_sequence (minimum 1, else 400) to resume after the last event you processed; events are delivered in strict sequence order and the stream closes after a terminal event.
If the run already ended at or before after_sequence, the stream closes immediately without emitting anything — use GET /v1/runs/{run_id}/events to read a finished run's history instead.
curl -N "https://<your-app-hostname>/v1/runs/$RUN_ID/stream?after_sequence=42" \
-H "Authorization: Bearer $DYNAMIQ_ACCESS_KEY"Event format
Both streaming endpoints emit standard SSE frames — an event: line with the event type and a data: line with the JSON payload — plus a : heartbeat comment every 15 seconds to keep the connection alive:
event: agent.response.delta
data: {"id":"...","type":"agent.response.delta","sequence":7,"timestamp":"2026-06-10T09:30:02Z","data":{"object":"agent.response.delta","delta":"Hello"},"source":{"id":"...","name":"Agent","group":"agents","type":"react"}}Every event has id, type, sequence, timestamp, type-specific data, and optionally checkpoint_id and a source (id, name, group, type) identifying the node that produced it.
Event types:
| Group | Types |
|---|---|
| Run lifecycle | run.created, run.started, run.paused, run.resumed, run.failed, run.completed, run.canceled |
| Agent output | agent.reasoning.delta, agent.response.delta, agent.info |
| Tool calls | agent.tool_call.initiated, agent.tool_call.input.delta, agent.tool_call.invoked, agent.tool_call.completed |
| Human in the loop | agent.human_feedback.requested, agent.human_feedback.received, approval_request.created, approval_request.confirmed, approval_request.rejected |
| LLM | llm.chat_completion.chunk |
run.completed carries the final output; run.failed carries an error with code and message; run.canceled carries a reason. The terminal event is always the last one on the stream.
Next steps
Call Your App over HTTP
The full HTTP contract for invoking a deployed Dynamiq App — auth, request body, synchronous responses, SSE streaming, async callbacks, and error codes.
Streaming & Async Jobs
Stream app output over SSE or WebSocket, run jobs asynchronously with callbacks, and resume paused runs with human feedback.