> ## Documentation Index
> Fetch the complete documentation index at: https://docs.asteroid.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Call a workflow from your code

> How to integrate against the Asteroid API in a few simple steps

## The contract

| Item               | Value                                                         |
| ------------------ | ------------------------------------------------------------- |
| Base URL           | `https://odyssey.asteroid.ai/agents/v2`                       |
| Auth header        | `X-Api-Key: <your-api-key>`                                   |
| Start an execution | `POST /agents/{agentId}/execute` → `{ "executionId": "..." }` |
| Read an execution  | `GET /executions/{executionId}`                               |

These are the basic calls that you will need to get started. Files, profiles, metadata, schedules and webhooks all build on them.

The execute call returns as soon as the execution starts. The execution itself continues on our infrastructure.
Poll the read call until the execution reaches a terminal status, or use webhooks to get alerted when the execution is finished.

<Tip>
  Writing this with a coding agent? Give it [https://docs.asteroid.ai/skill.md](https://docs.asteroid.ai/skill.md)
</Tip>

***

## A complete program

This program starts an execution, polls it to a terminal status, and branches on the result.
Set `ASTEROID_API_KEY` and `ASTEROID_AGENT_ID`, then replace the inputs and the outcome labels
with your workflow's own.

<CodeGroup>
  ```ts TypeScript theme={null}
  // npm install asteroid-odyssey
  import { client, agentExecutePost, executionGet } from 'asteroid-odyssey';

  const TERMINAL = ['completed', 'failed', 'cancelled'];
  const WAITING_FOR_A_PERSON = ['paused', 'paused_by_agent', 'awaiting_confirmation'];

  client.setConfig({
    baseUrl: 'https://odyssey.asteroid.ai/agents/v2',
    headers: { 'X-Api-Key': process.env.ASTEROID_API_KEY! },
  });

  async function runAgent(agentId: string, inputs: Record<string, unknown>) {
    const { data, error } = await agentExecutePost({
      path: { agentId },
      body: { inputs },
    });
    if (error) throw new Error(`execute failed: ${JSON.stringify(error)}`);

    const executionId = data!.executionId;
    const deadline = Date.now() + 30 * 60 * 1000;

    while (Date.now() < deadline) {
      await new Promise((resolve) => setTimeout(resolve, 5000));

      const { data: execution, error: pollError } = await executionGet({ path: { executionId } });
      if (pollError) throw new Error(`poll failed: ${JSON.stringify(pollError)}`);
      if (!execution) continue;

      if (TERMINAL.includes(execution.status)) return execution;

      if (WAITING_FOR_A_PERSON.includes(execution.status)) {
        throw new Error(
          `${executionId} waits for a person (${execution.status}): ${execution.platformUrl}`,
        );
      }
    }

    throw new Error(`${executionId} did not finish inside 30 minutes`);
  }

  const execution = await runAgent(process.env.ASTEROID_AGENT_ID!, {
    patientName: 'Jane Doe',
  });

  // failed and cancelled runs carry no executionResult.
  if (execution.status !== 'completed' || !execution.executionResult) {
    throw new Error(`run ended ${execution.status}: ${execution.platformUrl}`);
  }

  const { outcome, result, reasoning } = execution.executionResult;

  switch (outcome) {
    case 'slots_found':
      console.log(result);
      break;
    case 'no_slots_available':
      console.log(reasoning);
      break;
    default:
      // A new outcome label is a change to the contract. Escalate, do not crash.
      throw new Error(`unhandled outcome "${outcome}": ${execution.platformUrl}`);
  }
  ```

  ```python Python theme={null}
  # pip install --upgrade asteroid-odyssey
  import os
  import time

  from asteroid_odyssey import ApiClient, Configuration
  from asteroid_odyssey.api.agents_api import AgentsApi
  from asteroid_odyssey.api.execution_api import ExecutionApi
  from asteroid_odyssey.models.agents_agent_execute_agent_request import (
      AgentsAgentExecuteAgentRequest,
  )

  TERMINAL = ("completed", "failed", "cancelled")
  WAITING_FOR_A_PERSON = ("paused", "paused_by_agent", "awaiting_confirmation")

  config = Configuration(api_key={"ApiKeyAuth": os.environ["ASTEROID_API_KEY"]})


  def run_agent(agent_id: str, inputs: dict):
      with ApiClient(config) as api_client:
          agents_api = AgentsApi(api_client)
          execution_api = ExecutionApi(api_client)

          response = agents_api.agent_execute_post(
              agent_id=agent_id,
              agents_agent_execute_agent_request=AgentsAgentExecuteAgentRequest(
                  inputs=inputs,
              ),
          )
          execution_id = response.execution_id
          deadline = time.time() + 30 * 60

          while time.time() < deadline:
              time.sleep(5)
              execution = execution_api.execution_get(execution_id=execution_id)

              if execution.status in TERMINAL:
                  return execution

              if execution.status in WAITING_FOR_A_PERSON:
                  raise RuntimeError(
                      f"{execution_id} waits for a person "
                      f"({execution.status}): {execution.platform_url}"
                  )

          raise TimeoutError(f"{execution_id} did not finish inside 30 minutes")


  execution = run_agent(os.environ["ASTEROID_AGENT_ID"], {"patientName": "Jane Doe"})

  # failed and cancelled runs carry no execution_result.
  if execution.status != "completed" or execution.execution_result is None:
      raise RuntimeError(f"run ended {execution.status}: {execution.platform_url}")

  outcome = execution.execution_result.outcome

  if outcome == "slots_found":
      print(execution.execution_result.result)
  elif outcome == "no_slots_available":
      print(execution.execution_result.reasoning)
  else:
      # A new outcome label is a change to the contract. Escalate, do not crash.
      raise RuntimeError(f'unhandled outcome "{outcome}": {execution.platform_url}')
  ```

  ```bash cURL theme={null}
  #!/usr/bin/env bash
  # Needs curl and jq.
  set -euo pipefail

  BASE="https://odyssey.asteroid.ai/agents/v2"
  AUTH="X-Api-Key: $ASTEROID_API_KEY"

  # 1. Start the run.
  EXECUTION_ID=$(curl -sS --fail-with-body -X POST "$BASE/agents/$ASTEROID_AGENT_ID/execute" \
    -H "$AUTH" \
    -H "Content-Type: application/json" \
    -d '{"inputs": {"patientName": "Jane Doe"}}' | jq -r .executionId)

  echo "started $EXECUTION_ID"

  # 2. Poll every 5 seconds until the status is terminal. 360 tries = 30 minutes.
  EXECUTION=""
  for _ in $(seq 1 360); do
    sleep 5
    EXECUTION=$(curl -sS --fail-with-body "$BASE/executions/$EXECUTION_ID" -H "$AUTH")
    STATUS=$(echo "$EXECUTION" | jq -r .status)
    echo "status: $STATUS"

    case "$STATUS" in
      completed|failed|cancelled)
        break
        ;;
      paused|paused_by_agent|awaiting_confirmation)
        echo "waits for a person: $(echo "$EXECUTION" | jq -r .platformUrl)" >&2
        exit 1
        ;;
    esac
  done

  case "$STATUS" in
    completed|failed|cancelled) ;;
    *)
      echo "$EXECUTION_ID did not finish inside 30 minutes (status: $STATUS)" >&2
      exit 1
      ;;
  esac

  # 3. Read the result.
  echo "$EXECUTION" | jq '{
    status,
    outcome: .executionResult.outcome,
    reasoning: .executionResult.reasoning,
    result: .executionResult.result,
    url: .platformUrl
  }'
  ```
</CodeGroup>

<Warning>
  Loop until the status is terminal. A loop written as `while (status === 'running')` exits the
  moment the workflow pauses. It then never sees the answer.
</Warning>

***

## What the program needs from you

Three things: a key, a published version, and the input names.

<Steps>
  <Step title="Create an API key">
    In the platform, click your profile picture at the bottom left, then **API Keys**. Or open
    [platform.asteroid.ai/keys](https://platform.asteroid.ai/keys).

    A key belongs to your organisation, not to one workflow. One key runs every workflow you own.

    ```bash theme={null}
    export ASTEROID_API_KEY="ast..."
    export ASTEROID_AGENT_ID="<your-agent-id>"
    ```

    Keep the key on your server. Never ship it in browser or mobile code.
  </Step>

  <Step title="Publish a version">
    The API runs the **published** version of a workflow. Edits you save in the builder stay in the
    draft until you publish them.

    Open the builder and click **Publish**. Publish again after every change you want in
    production. See [Versions and publishing](/concepts/versions).

    <Warning>
      An unpublished workflow returns an error when you call it. If your integration runs old
      behaviour, someone edited the draft and did not publish it.
    </Warning>
  </Step>

  <Step title="Read the workflow's inputs">
    Every workflow declares named inputs. Each input has a name, a type, and a required flag. You send
    them as the `inputs` object.

    The workflow's deploy page lists the exact set, at
    `https://platform.asteroid.ai/agents/<agent-id>/deploy`. It also shows the workflow ID, the
    outcome labels, and a ready-to-run request. Copy the names from there. Do not guess them from
    the instructions.
  </Step>
</Steps>

***

## Inputs

Send one key per declared input.

```json theme={null}
{
  "inputs": {
    "patientName": "Jane Doe",
    "dateOfBirth": "1984-02-11",
    "appointmentTypes": ["annual physical", "follow-up"]
  }
}
```

The rules:

* Input names match `[a-zA-Z_][a-zA-Z0-9_]*` and are unique inside the workflow.
* Asteroid validates your values against the declared schema when the execution starts.
* A required input with no value fails the run at once, not halfway through.
* An omitted optional input falls back to its default.

See [Inputs and outputs](/concepts/inputs-and-outputs) for how a workflow declares them.

***

## Every field on the execute body

Every field is optional. A bare `{}` runs a workflow that declares no required inputs.

<ParamField body="inputs" type="object">
  Values for the inputs the workflow declares. Keys must match the input names on the deploy page.
</ParamField>

<ParamField body="agentProfileId" type="uuid">
  Run with one named [agent profile](/concepts/profiles) — its credentials, cookies, proxy, and
  saved browser state. Mutually exclusive with `agentProfilePoolId`.
</ParamField>

<ParamField body="agentProfilePoolId" type="uuid">
  Take a free profile from a pool. Use a pool when you run the same workflow against one portal more
  than once at a time. Two concurrent executions on one profile overwrite each other's session state.
  Mutually exclusive with `agentProfileId`.
</ParamField>

<ParamField body="tempFiles" type="array">
  Files staged before the execution. Stage them with `POST /temp-files/{organizationId}`, then pass the
  `tempFiles` array from that response straight through. Staged files expire after 60 minutes. See
  [Workflow filesystem](/concepts/filesystem).
</ParamField>

<ParamField body="metadata" type="object">
  String keys and string values you attach to the execution. Metadata never reaches the workflow's
  instructions. You can filter executions by it later.
</ParamField>

<ParamField body="version" type="integer">
  Run a specific version instead of the published one. Pin a version for a regression test, or for
  a customer on a fixed contract. Leave it out for normal traffic. See
  [Versions and publishing](/concepts/versions).
</ParamField>

<ParamField body="executionOptions" type="object">
  Per-run overrides. `softTimeoutMins` tells the workflow to wrap up after that many minutes.
  `variantKey` scopes the execution's shared files when the workflow uses variant mode.
</ParamField>

<ParamField body="onCapacityLimit" type="string" default="reject">
  What happens when the execution cannot start right now: your organisation is at its concurrency
  limit, or every profile in the pool is in use. `reject` (the default) fails the request with
  `429`. `queue` accepts it instead: you get the execution ID at once with
  `"status": "queued"`, and the execution starts by itself, oldest first, as capacity frees. Poll it
  by ID as you would any other. Neither value changes the `400` for a pool with no profiles or an
  organisation with a limit of `0`. A request that sets `queue` cannot carry `tempFiles`, even when
  capacity is free.
</ParamField>

A request that uses several of them:

```json theme={null}
{
  "inputs": { "patientName": "Jane Doe" },
  "agentProfilePoolId": "3b71...",
  "metadata": {
    "orderId": "ORD-88213",
    "tenant": "northside-clinic",
    "environment": "production"
  },
  "executionOptions": { "softTimeoutMins": 20 }
}
```

<Tip>
  Put your own record ID in `metadata`. A webhook handler then knows which row to update. You can
  also search for the execution before you retry it.
</Tip>

### Queue when capacity is full

Your organisation has a concurrency limit. A profile pool with `allowConcurrentUse: false` has one
slot per profile. By default, an execute call that finds no room fails with `429`, whether the
organisation limit or the pool is what is full. Send `"onCapacityLimit": "queue"` to wait instead.

```bash theme={null}
curl -sS -X POST "$BASE/agents/$ASTEROID_AGENT_ID/execute" \
  -H "X-Api-Key: $ASTEROID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "agentProfilePoolId": "3b71...", "onCapacityLimit": "queue" }'
```

```json theme={null}
{ "executionId": "947997d6-...", "status": "queued" }
```

The call returns `202` either way. `status` is `starting` when the execution got a slot and
`queued` when it did not. A queued execution starts by itself when a slot frees, oldest first, and
then runs like any other. Poll it by ID as usual. Cancel it through the executions API and it never
starts.

Limits:

* 5,000 queued executions per organisation. Beyond that the call returns `429`.
* A request that sets `queue` cannot carry `tempFiles`.
* An empty pool, or an organisation with a limit of `0`, returns `400` instead of a queue item.
  Nothing would ever release it.

***

## Reading the result

`GET /executions/{executionId}` returns everything about an execution.

```bash theme={null}
curl "https://odyssey.asteroid.ai/agents/v2/executions/$EXECUTION_ID" \
  -H "X-Api-Key: $ASTEROID_API_KEY"
```

### Status tells you whether the execution finished

| Status                  | Phase    | Meaning                                                                                                                                                                    |
| ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queued`                | Pending  | The platform holds the execution before it may begin. Batch rows, future-dated runs and execute calls that [waited for capacity](#queue-when-capacity-is-full) start here. |
| `starting`              | Active   | Accepted. The environment is booting.                                                                                                                                      |
| `running`               | Active   | The workflow is working.                                                                                                                                                   |
| `paused`                | Active   | A person paused it.                                                                                                                                                        |
| `paused_by_agent`       | Active   | The workflow asked a question and waits for an answer.                                                                                                                     |
| `awaiting_confirmation` | Active   | The workflow waits for a person to approve a step.                                                                                                                         |
| `completed`             | Terminal | The execution finished and produced a result.                                                                                                                              |
| `failed`                | Terminal | The execution could not finish.                                                                                                                                            |
| `cancelled`             | Terminal | Someone or something stopped it.                                                                                                                                           |

Those nine are the complete set. **Terminal means finished: `completed`, `failed`, `cancelled`.**
Nothing leaves a terminal status.

<Note>
  There is no `timed_out` status. An execution past its timeout ends `failed`, or ends `cancelled` with
  the reason `timeout`. The cancel reasons are `user_requested`, `timeout`, `no_activity`,
  `budget_exceeded`, `script_failed` and `max_steps`.
</Note>

A direct API call starts at `starting`, or at `queued` when it [waited for capacity](#queue-when-capacity-is-full). See [Executions and statuses](/concepts/executions) for the
full lifecycle.

### Outcome tells you what happened

`completed` means the workflow reached an Output node. It does not mean the work succeeded.

An execution that correctly reports "this patient has no coverage" is `completed`. So is an execution that booked
the appointment. The **outcome label** separates the two.

```json theme={null}
{
  "id": "0a19...",
  "agentId": "7c22...",
  "workflowId": "91ab...",
  "status": "completed",
  "createdAt": "2026-03-27T11:01:40Z",
  "startedAt": "2026-03-27T11:01:48Z",
  "terminalAt": "2026-03-27T11:04:16Z",
  "duration": 148.2,
  "inputs": { "patientName": "Jane Doe" },
  "metadata": { "orderId": "ORD-88213" },
  "executionResult": {
    "id": "5f30...",
    "executionId": "0a19...",
    "outcome": "no_slots_available",
    "reasoning": "The portal returned an empty calendar for the requested week.",
    "result": { "slots": [], "nextAvailable": "2026-04-08" },
    "createdAt": "2026-03-27T11:04:12Z"
  },
  "agentProfileId": "8f0d...",
  "agentProfileName": "northside-clinic",
  "recordingUrl": "https://...",
  "liveViewUrl": "https://...",
  "platformUrl": "https://platform.asteroid.ai/agents/.../executions/..."
}
```

| Field                                  | Type   | Use it for                                                  |
| -------------------------------------- | ------ | ----------------------------------------------------------- |
| `executionResult.outcome`              | string | Branching. One of the outcome labels the workflow declares. |
| `executionResult.result`               | object | The data, matching the workflow's Result Schema.            |
| `executionResult.reasoning`            | string | Logs and support tickets. Never branch on it.               |
| `status`                               | string | Deciding whether the execution finished at all.             |
| `duration`                             | number | Seconds the execution took. Terminal runs only.             |
| `inputs`                               | object | The values the execution started with.                      |
| `metadata`                             | object | Your own IDs, exactly as you sent them.                     |
| `platformUrl`                          | string | A link a person opens to see what happened.                 |
| `recordingUrl`                         | string | The session recording, once the execution is terminal.      |
| `liveViewUrl`                          | string | Watch a running workflow live.                              |
| `agentProfileId`, `agentProfileName`   | string | Which profile the execution used.                           |
| `createdAt`, `startedAt`, `terminalAt` | string | Timestamps for the execution.                               |

**Branch on `outcome`, not on `status`.** The outcome labels are the contract between the workflow and
your code. Treat a new label as a breaking change, and handle the unknown label rather than
crashing on it. See [Inputs and outputs](/concepts/inputs-and-outputs).

<Warning>
  `executionResult` is absent when an execution never reaches an Output node. Check that it exists before
  you read it.
</Warning>

<Info>
  JSON and the TypeScript SDK use camelCase, so `executionResult`. The Python SDK exposes
  snake\_case attributes, so `execution.execution_result`.
</Info>

***

## When an execution waits for a person

Two statuses mean the workflow needs an answer before it continues.

* `paused_by_agent` — the workflow asked a question.
* `awaiting_confirmation` — the workflow wants approval before it acts.

Reply with a user message, and the execution continues.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://odyssey.asteroid.ai/agents/v2/executions/$EXECUTION_ID/user-messages" \
    -H "X-Api-Key: $ASTEROID_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"message": "Confirmed. Book the 09:00 slot."}'
  ```

  ```ts TypeScript theme={null}
  import { executionUserMessagesAdd } from 'asteroid-odyssey';

  await executionUserMessagesAdd({
    path: { executionId },
    body: { message: 'Confirmed. Book the 09:00 slot.' },
  });
  ```

  ```python Python theme={null}
  execution_api.execution_user_messages_add(
      execution_id=execution_id,
      agents_execution_user_messages_add_text_body={
          "message": "Confirmed. Book the 09:00 slot."
      },
  )
  ```
</CodeGroup>

An unattended integration cannot answer. Treat both statuses as an escalation there: stop the poll
loop, and send `platformUrl` to whoever handles exceptions. The program above does this.

***

## Cancel an execution

```bash theme={null}
curl -X POST "https://odyssey.asteroid.ai/agents/v2/executions/$EXECUTION_ID/status" \
  -H "X-Api-Key: $ASTEROID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "cancelled"}'
```

The endpoint accepts `running`, `paused` and `cancelled`. It rejects any change to an execution that
already reached a terminal status.

***

## Polling or webhooks

<CardGroup cols={2}>
  <Card title="Polling" icon="refresh-cw" horizontal>Simple, and it needs no public endpoint. Poll every 5-10 seconds. Good for runs under about 5 minutes.</Card>
  <Card title="Webhooks" icon="webhook" href="/operate/webhooks-and-slack" horizontal>We call you on every status change. Better for long executions and high volume. Payloads are signed.</Card>
</CardGroup>

Faster polling does not finish an execution sooner. Set a deadline on every poll loop. A single-node workflow
finishes in 30 to 60 seconds. A multi-node workflow takes several minutes.

Run both if you want. Webhooks drive the normal path, and a sweep every few minutes catches
anything your endpoint missed while it was down. Sweep with `GET /executions`.

See [Webhooks and Slack](/operate/webhooks-and-slack) to set up notifications.

***

## Look deeper into an execution

| What you want               | Call                                          |
| --------------------------- | --------------------------------------------- |
| Step-by-step timeline       | `GET /executions/{executionId}/activities`    |
| Files the workflow produced | `GET /executions/{executionId}/agent-files`   |
| Files you attached          | `GET /executions/{executionId}/context-files` |
| All runs, filtered          | `GET /executions`                             |

`GET /executions` filters by `agentId`, `status`, `phase`, `outcomeLabel`, `triggerSource`,
`workflowVersion`, `metadataKey`, `metadataValue`, `inputsKey`, `inputsValue`, `createdAfter`,
`createdBefore`, `agentProfileIds`, `humanLabels` and `hasScriptFailures`.

Use it to build a dashboard, to sweep for missed runs, or to check for work you already ran:

```bash theme={null}
curl "https://odyssey.asteroid.ai/agents/v2/executions?agentId=$ASTEROID_AGENT_ID\
&metadataKey=orderId&metadataValue=ORD-88213" \
  -H "X-Api-Key: $ASTEROID_API_KEY"
```

***

## Common errors

| Symptom                                                                                    | Cause                                                              | Fix                                                    |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------ |
| `401 Unauthorized`                                                                         | Wrong header name                                                  | Use `X-Api-Key`, not `Authorization`                   |
| `404 Not Found` on execute                                                                 | Wrong workflow ID, or the workflow belongs to another organisation | Copy the ID from the deploy page                       |
| Error on execute, or the execution uses old behaviour                                      | The version is still a draft                                       | Publish the version                                    |
| Poll loop exits at once                                                                    | The loop tests `status === 'running'`                              | Loop until the status is terminal                      |
| Poll loop never ends                                                                       | The workflow waits for a person                                    | Handle `paused_by_agent` and `awaiting_confirmation`   |
| The workflow ignores your inputs                                                           | An input name does not match                                       | Use the names on the deploy page, spelled the same way |
| `executionResult` is missing                                                               | The execution ended `failed` or `cancelled`                        | Check `status` first, then read `executionResult`      |
| `429 Concurrency limit exceeded` or `429 All profiles ... are currently in use` on execute | Your organisation or profile pool is full                          | Send `"onCapacityLimit": "queue"`, or retry later      |
| `429 The execution queue is full`                                                          | 5,000 executions are already waiting                               | Wait for some to start before you submit more          |

Nothing appears on the **Executions** page? Then the request never reached us. Check the base URL
and the header name.

***

## Next

<CardGroup cols={2}>
  <Card title="Production checklist" icon="circle-check" href="/integrate/production-checklist" horizontal>What to settle before real traffic arrives</Card>
  <Card title="Agent profiles" icon="id-card" href="/concepts/profiles" horizontal>Credentials, cookies, proxies, and pools</Card>
  <Card title="Batch executions" icon="layers" href="/operate/batches" horizontal>Run one workflow over many rows, with concurrency limits</Card>
  <Card title="Executions and statuses" icon="hourglass" href="/concepts/executions" horizontal>The full lifecycle and its transition rules</Card>
  <Card title="TypeScript SDK" icon="code" href="/sdks/typescript" horizontal>Client setup and the full function list</Card>
  <Card title="Python SDK" icon="terminal" href="/sdks/python" horizontal>Client setup and the API classes</Card>
</CardGroup>
