Start a run for a platform-managed agent whose value is a generated file — PDF report, spreadsheet export, rendered image — and stream the runtime's binary response straight through to the caller. The platform never buffers the body and never stores it; only metadata (filename, mime type, byte count) is recorded on the run row for tracking.
Endpoint
POST /api/agents/{agentId}/runs/trigger-and-download
Auth: Authorization: Bearer <ingest_api_key>
Eligibility
managed_by = 'platform'status = 'active'allow_external_triggers = trueoutput_kind = 'file_download'— set this on the agent settings page
Anything else returns 422 with an explanatory message.
Runtime support
The workflow MUST terminate in a Respond to Webhook node configured to return binary data — typically a previous node's binary output bound to "Response Data" — with Content-Type (application/pdf, text/csv, image/png, …) and Content-Disposition (attachment; filename="report.pdf") headers set on the response. The platform passes both headers through to the caller verbatim.
If the workflow returns a JSON body instead (for example because a file-producing node failed), the platform surfaces it as a failed run and returns 502 with the parsed JSON — not a corrupt binary.
Optional X-Outputs response header
The runtime may include X-Outputs: <non-negative integer> on the response. The platform lifts that value into runs.outputs — the same metric column the JSON outputs field populates on the trigger-and-wait endpoint. For a one-file-per-run agent, 1 is the natural value. Anything missing or non-integer is silently dropped (same rule as the async callback flow).
Request
{
"input": { "topic": "Q3 financial summary" },
"timeoutMs": 120000
}
| Field | Type | Required | Description |
|---|---|---|---|
input | object | no | Merged on top of the agent's saved default input; caller's values win. |
userId | uuid | no | Attribution for runs.created_by. Unknown / non-member values are dropped silently. |
timeoutMs | number | no | Maximum time the server may wait for the runtime to begin streaming, in milliseconds. Server further caps at the agent's running_timeout_ms and a platform hard ceiling. Defaults to 60 000 ms when omitted. |
Response — 200 OK (streaming)
The runtime began streaming within the wait window. Response headers carry the platform run id and the runtime's content metadata; the response body is the binary file.
| Header | Description |
|---|---|
Content-Type | Pass-through of the runtime's content type (e.g. application/pdf). |
Content-Disposition | Pass-through of the runtime's disposition. Synthesized as attachment; filename="<derived>" if the runtime omitted it. |
Content-Length | Pass-through when the runtime sent one (omitted on chunked transfers). |
X-Run-Id | Platform-side run id. Use it to correlate with GET /api/agents/{agentId}/runs/{runId}. |
Cache-Control | Always no-store. The bytes are run-specific and uncacheable. |
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="q3-summary.pdf"
Content-Length: 124583
X-Run-Id: 1234
Cache-Control: no-store
<binary pdf bytes>
Response — 502 Bad Gateway (runtime returned a JSON error)
The runtime executed but returned a JSON payload instead of a binary file — the typical shape of a failed workflow. The run row is marked failed. The body is JSON, not a binary; do not save it as a file.
{
"error": "WorkflowReturnedJson: { \"error\": \"PDF rendering failed\" }",
"runId": 1234
}
Other errors
| Status | Reason |
|---|---|
400 | Invalid request body. |
401 | Missing or invalid bearer token. |
403 | allow_external_triggers is off for this agent. |
422 | Agent is not platform-managed proper runtime environment with output_kind = 'file_download', or is not active. |
429 | Per-agent rate limit exceeded (5/min — same posture as /trigger-and-wait; each call holds a connection for up to the wait window). |
504 | Upstream runtime did not respond within the wait window. The run row is marked failed. |
500 | Runtime invocation failed before streaming could begin. |
What lands on the run row
The streamed bytes are never stored on the platform. Instead, runs.output_data carries a small metadata object so the run-detail page can render a summary:
{
"kind": "file_download",
"filename": "q3-summary.pdf",
"mimeType": "application/pdf",
"sizeBytes": 124583
}
runs.outputs is populated from the optional X-Outputs response header (see above). duration_ms and completed_at are stamped when the stream finishes flushing, so they reflect end-to-end generation + transfer time.
Re-download is not available — the file isn't replayable. Trigger another run to regenerate.
Choosing between the three sync endpoints
Use /trigger | Use /trigger-and-wait | Use /trigger-and-download |
|---|---|---|
| Caller doesn't need output inline | Output is a JSON payload the caller renders | Output is a binary file the caller hands to the end user |
| Caller polls or waits for a callback | Caller wants the JSON in the same response | Caller wants the bytes streamed to disk or to a browser |
| Run may take longer than a few minutes | Run completes in seconds or low minutes | Run completes in seconds or low minutes |
SDK
import { triggerRunAndDownload } from '@cxpa/sdk/agent'
import { writeFile } from 'node:fs/promises'
import { Readable } from 'node:stream'
const result = await triggerRunAndDownload({
baseUrl: process.env.CXPA_API_URL!,
ingestApiKey: process.env.CXPA_INGEST_KEY!,
agentId: 42,
input: { topic: 'Q3 financial summary' },
})
await writeFile(result.filename, Readable.fromWeb(result.body))
console.log(`Saved ${result.filename} (run #${result.runId})`)
Consume result.body exactly once — the SDK does not buffer it. In a web handler, forward it straight to the caller's response so the browser saves the file without any intermediate buffering on your side.