Start a project Run and read its result
Dispatch a project's configured agent, follow the Run to a terminal status, and read its output.
This guide dispatches the agent configured on an existing Project with the Developer API, follows the Run until it reaches a terminal status, and reads its output. The project-scoped route uses the Project's agent template, so the request supplies only a command key and optional input; it does not need a prompt, model, tools, or context identifiers.
Before you start
You need:
- a Project whose agent template is configured;
- an API key with the
automationmanagecapability and therun.dispatchaction grant. The reads in this guide require theautomationreadcapability, whichmanagealso covers. Listing projects requires theprojectsreadcapability.
See API keys to create a key, and API overview for capability levels, action grants, and the error contract.
Set the origin and key once for the examples:
export API_ORIGIN="https://api.factorai.ai"
export API_KEY="<your API key>"Read the Project's teamId and id from the projects visible to the key:
PROJECT=$(curl -sS "$API_ORIGIN/v1/projects" \
-H "Authorization: Bearer $API_KEY" |
jq -c '.projects[] | select(.slug == "my-project")')
TEAM_ID=$(printf '%s' "$PROJECT" | jq -r '.teamId')
PROJECT_ID=$(printf '%s' "$PROJECT" | jq -r '.id')Steps
1. Dispatch the Run
POST /v1/teams/{teamId}/projects/{projectId}/runs accepts a required
commandKey and an optional JSON input. commandKey is the idempotency key:
a retry that reuses it with the same body replays the original dispatch instead
of starting another Run. input is passed to the Project's agent template.
Generate the key once and keep it with the request body until the dispatch
succeeds. Reuse both unchanged after a lost response; use a new key only for a
separate intended Run.
# Generate this once, then save and reuse it with the same request body on retry.
COMMAND_KEY="project-run-$(date +%s)-$RANDOM"
RUN=$(curl -sS -X POST \
"$API_ORIGIN/v1/teams/$TEAM_ID/projects/$PROJECT_ID/runs" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"commandKey\": \"$COMMAND_KEY\",
\"input\": {
\"objective\": \"Summarize this week's open issues.\"
}
}")
RUN_ID=$(printf '%s' "$RUN" | jq -r '.run.id')
ATTEMPT_ID=$(printf '%s' "$RUN" | jq -r '.attempt.id')The response carries the run, its immutable revision, the first attempt,
a created flag, and integrationRequired.
2. Interpret the response
| Status | Meaning |
|---|---|
201 | A new Run was created. created is true; read run.id. |
200 | The command key replayed an earlier dispatch. created is false. |
202 | The policy requires approval before the Run starts. |
A 200 replay returns the same run, revision, and attempt as the original
dispatch, so a retried request is safe to handle the same way as a 201.
A 202 response carries action, approval.requestId, and
approval.status: "approval_required". The persisted request executes once an
approver resolves it; there is no Run ID to poll yet. Use a fresh commandKey
only when you intend a separate dispatch.
3. Connect required integrations
When the Project's agent needs an integration the Run cannot use yet,
integrationRequired lists each entry as { integration, setupUrl, tools }.
Connect each integration at its setupUrl; the Run waits and resumes on its
own. An empty array means nothing is missing.
printf '%s' "$RUN" |
jq '.integrationRequired[] | { integration, setupUrl, tools }'The same array appears on GET /v1/runs/{runId} while an Attempt is waiting, so
recheck it if the Run does not start.
4. Poll the Run
GET /v1/runs/{runId} returns the Run and its execution history. Repeat the
read while run.status is pending, running, or waiting. completed,
failed, and cancelled are terminal; on failed, read the Attempt's error
for the cause.
while :; do
SNAPSHOT=$(curl -sS "$API_ORIGIN/v1/runs/$RUN_ID" \
-H "Authorization: Bearer $API_KEY")
STATUS=$(printf '%s' "$SNAPSHOT" | jq -r '.run.status')
case "$STATUS" in
pending | running | waiting) sleep 5 ;;
*) break ;;
esac
done
printf '%s' "$SNAPSHOT" |
jq '{status: .run.status, attempts: [.attempts[] | {id, status, error, waitingReason, resultArtifactIds}]}'A waiting Attempt also reports waitingReason (approval, policy,
dependency, or user).
5. Read the result
GET /v1/runs/{runId}/result returns the selected Attempt's status, error,
waitingReason, durable assistant output, and final artifacts. By default
it selects the latest Attempt on the newest revision; pass runAttemptId to
read a specific Attempt.
curl -sS "$API_ORIGIN/v1/runs/$RUN_ID/result" \
-H "Authorization: Bearer $API_KEY" |
jq '{status, output, error, artifacts}'output is null until the Attempt produces durable output, and artifacts
carries the final artifacts with resolved download URLs. A 503 with
run_output_unavailable means the stored output could not be read; retry the
read.
Errors
The dispatch route documents these failures:
400- the dispatch body was rejected as an invalid Run request.401- the request is missing a valid API key.403- the key lacks theautomationmanagecapability, therun.dispatchaction grant, or access to the Project.404- the Project does not exist for the key.409- the Project is not dispatchable (no agent template, unknown template, or invalid configuration), or the Run is halted.422- the Run request is invalid.
The reads in this guide return 401, 403, 404, and, for the result read,
503. See API overview for the full error contract.
Next steps
- API overview - authentication, capabilities, and endpoint families
- Workflow and Run lifecycle - how Runs, revisions, and Attempts relate
- Start a workflow - the Workflow task guide
- Command-line interface - install, authenticate, and automate the CLI