> ## 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-Asteroid-Agents-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-Asteroid-Agents-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-Asteroid-Agents-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>

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>

***

## 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-Asteroid-Agents-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 and future-dated runs 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`. 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-Asteroid-Agents-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-Asteroid-Agents-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-Asteroid-Agents-Api-Key: $ASTEROID_API_KEY"
```

***

## Common errors

| Symptom                                               | Cause                                                              | Fix                                                    |
| ----------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------ |
| `401 Unauthorized`                                    | Wrong header name                                                  | Use `X-Asteroid-Agents-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`      |

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>
