# Call Your Agent from Your Product

> Wire a FlyMy.AI agent into your product over plain REST: upload a file, start a run via run-loop or a frozen run-instruction, poll the execution, read agent_result.

You built an agent that does the job - in the app, from Claude, or with the SDK. This guide turns it into a backend feature of your own product using four plain HTTPS calls. Every step shows raw `curl` first and the Python equivalent next to it.

> **Tip: Start with the complete lifecycle**
>
> If you still need to create the agent, choose variables, freeze it, or decide
> between an owner run and per-customer MCP access, start with
> [Create, Freeze, and Embed One Agent](https://docs.flymy.ai/agents/guides/serverless-agents.md). This
> page is the lower-level execution and polling reference.

The whole integration is:

1. Upload a file the run should work on (optional) - get back a `public_url`
2. Start a run - a frozen compilation via `run-instruction` (the production default) or a live agent via `run-loop`
3. Poll the execution every ~2 seconds until it reaches a terminal status
4. Read `agent_result`

> **Caution: An agent run is not a request-response call**
>
> A run takes roughly tens of seconds - the agent plans, calls tools, and assembles a result. Fire and forget from your hot path: the POST that starts the run returns immediately with an execution id. Store that id and poll from a background job. Never make an end-user request wait synchronously on a run.

## The contract at a glance

Base URL is `https://backend.flymy.ai`. Auth is the same `X-API-KEY: fly-***` header on every call - get a key at [app.flymy.ai/profile](https://app.flymy.ai/profile).

| Call | Purpose |
|---|---|
| `POST /api/v1/agents/agent-file-chat-upload/` | Multipart upload (`file`, `external_id`) - returns `{"public_url"}` |
| `POST /api/v1/agents/tasks/{agent_uuid}/run-loop/` | Start a live run with a caller-owned `Idempotency-Key` and `{"variables": {...}}` - returns `{"id"}` |
| `POST /api/v1/agents/compilations/{compilation_id}/run-instruction/` | Start a frozen owner run with a persisted `Idempotency-Key` and `{"variables": {...}}` - returns a new run |
| `GET /api/v1/agents/executions/{id}/` | Current run state - poll until `completed` / `failed`, then read `agent_result` |
| `POST /api/v1/agents/compilations/freeze-instruction/{execution_id}/` | Freeze a good run into a compilation you can `run-instruction` from then on |

You address a live agent by its UUID - grab it from `client.agents.list()` in the [Python SDK](https://docs.flymy.ai/agents/sdk/python.md), from the `list_agents` gateway tool, or from the agent's chat URL in the app (`app.flymy.ai/agents/chat/{agent_uuid}`).

The running example below is an agent that turns a voice memo into a structured note - upload the audio, run, poll, render the note in your UI.

## 1. Upload input files (optional)

Skip this step if your run only needs text variables. When the run needs a file from your product - an audio memo, an image, a document - upload it first and pass the returned `public_url` to the run as a variable.

**curl**

```bash
curl -X POST https://backend.flymy.ai/api/v1/agents/agent-file-chat-upload/ \
  -H "X-API-KEY: $FLYMYAI_API_KEY" \
  -F "file=@voice-memo.m4a" \
  -F "external_id=user-4242"
```

**Python**

```python
import requests

resp = requests.post(
    "https://backend.flymy.ai/api/v1/agents/agent-file-chat-upload/",
    headers={"X-API-KEY": "fly-***"},
    files={"file": open("voice-memo.m4a", "rb")},
    data={"external_id": "user-4242"},
)
public_url = resp.json()["public_url"]
```

`external_id` is an identifier you choose on your side (a user or session id works well). The response carries the field you need:

```json
{"public_url": "https://..."}
```

## 2. Start the run

There are two ways to start a run, and they matter differently in production:

- **Frozen compilation** (`run-instruction`) - replays a fixed instruction distilled from a run you approved. Deterministic, cheaper, faster. This is what you want behind a product feature. If you don't have a compilation id yet, see [Freeze your agent](#4-freeze-your-agent-for-production) below.
- **Live agent** (`run-loop`) - the agent re-plans from its goal on every run. Right for interactive use and for producing the good run you will freeze.

**curl**

```bash
# Production default: run a frozen compilation
curl -X POST https://backend.flymy.ai/api/v1/agents/compilations/$COMPILATION_ID/run-instruction/ \
  -H "X-API-KEY: $FLYMYAI_API_KEY" \
  -H "Idempotency-Key: $PERSISTED_RUN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"variables": {"audio_url": "'"$PUBLIC_URL"'"}}'

# Or: run a live agent by UUID
curl -X POST https://backend.flymy.ai/api/v1/agents/tasks/$AGENT_UUID/run-loop/ \
  -H "X-API-KEY: $FLYMYAI_API_KEY" \
  -H "Idempotency-Key: $PERSISTED_LIVE_RUN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"variables": {"audio_url": "'"$PUBLIC_URL"'"}}'
```

**Python**

```python
from flymyai import AgentClient

client = AgentClient(api_key="fly-***")

# Production default: run a frozen compilation
run = client.compilations.run_instruction(
    compilation_id,
    variables={"audio_url": public_url},
    idempotency_key=persisted_frozen_run_key,
)

# Or: run a live agent by UUID
run = client.runs.create(
    agent_id=agent_uuid,
    variables={"audio_url": public_url},
    idempotency_key=persisted_live_run_key,
)
```

Both calls return immediately - the run executes asynchronously on the server. The response gives you the execution id (a string slug):

```json
{"id": "won-gsfr-mxp"}
```

Before any run start, reserve a caller-owned nonblank printable ASCII key of
1-255 characters with no leading or trailing spaces and store it with the
canonical target and body in durable storage. Use
`PERSISTED_RUN_KEY` for the frozen call and `PERSISTED_LIVE_RUN_KEY` for the
live call. Reuse a key only for the identical replay and never let an SDK or
server generate a fallback. File upload and freeze do not share this replay
contract, so reconcile their ambiguous outcomes instead of repeating them.

`variables` are validated against the agent's `input_schema` and rendered into the goal for this run - see [Inputs, Outputs & Variables](https://docs.flymy.ai/agents/concepts/inputs.md).

## 3. Poll until it finishes, read agent_result

Poll `GET /api/v1/agents/executions/{id}/` about every 2 seconds until `status` is terminal (`completed`, `failed`, or `cancelled`), then read `agent_result`.

**curl**

```bash
EXECUTION_ID="won-gsfr-mxp"

while :; do
  RUN=$(curl -s https://backend.flymy.ai/api/v1/agents/executions/$EXECUTION_ID/ \
    -H "X-API-KEY: $FLYMYAI_API_KEY")
  STATUS=$(echo "$RUN" | jq -r .status)
  echo "status: $STATUS"
  case "$STATUS" in completed|failed|cancelled) break ;; esac
  sleep 2
done

echo "$RUN" | jq .agent_result
```

**Python**

```python
# SDK: polls every 2 seconds by default
result = client.runs.wait(run.id, timeout=600, poll_interval=2.0)
print(result.output)  # alias for agent_result

# Frozen one-liner: start + poll in a single call
result = client.compilations.run_instruction_and_wait(
    compilation_id,
    variables={"audio_url": public_url},
    idempotency_key=persisted_frozen_run_key,
    timeout=600,
)

# No SDK? The same loop in plain requests:
import time, requests

def wait_for_result(execution_id: str, timeout: float = 600.0) -> dict:
    headers = {"X-API-KEY": "fly-***"}
    url = f"https://backend.flymy.ai/api/v1/agents/executions/{execution_id}/"
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        run = requests.get(url, headers=headers).json()
        if run["status"] in ("completed", "failed", "cancelled"):
            return run
        time.sleep(2)
    raise TimeoutError(f"run {execution_id} did not finish in {timeout}s")
```

A completed execution looks like this (trimmed to the fields you need):

```json
{
  "id": "won-gsfr-mxp",
  "status": "completed",
  "agent_result": {
    "title": "Standup notes",
    "summary": "Shipped the importer, blocked on billing review...",
    "action_items": ["Ping legal about the DPA", "Cut release v2.3"]
  }
}
```

The shape of `agent_result` is whatever the agent's `output_schema` defines, so your product code can rely on the fields being there. On `failed`, the run carries a plain error string instead (surfaced as `.error` in the SDK) - see [Runs](https://docs.flymy.ai/agents/concepts/runs.md) for the full lifecycle.

> **Tip: Poll at ~2 seconds, not faster**
>
> Runs take tens of seconds; polling every 2 seconds finds the result within moments of it landing. Hammering the endpoint more often does not make the agent finish sooner.

## 4. Freeze your agent for production

A live agent re-plans on every run - great while you iterate, wasteful once the pipeline is settled. Freezing distills a run you approved into a fixed Markdown instruction that future runs replay: deterministic, cheaper, faster. ("Freeze" and "compile" are the same operation; the SDK surface is `client.compilations`.)

The flow:

1. Run the agent live (in the app chat or via `run-loop`) until one run does exactly what you want.
2. Freeze that execution:

**curl**

```bash
curl -X POST https://backend.flymy.ai/api/v1/agents/compilations/freeze-instruction/$EXECUTION_ID/ \
  -H "X-API-KEY: $FLYMYAI_API_KEY"
```

**Python**

```python
# Freeze + poll until the compilation reaches "compiled"
compilation = client.agents.compile_from_run(run.id, timeout=120)
print(compilation.id)              # use this in run-instruction
print(compilation.instruction_md)  # the frozen plan
```

3. The freeze call returns immediately and compilation starts in the background (status begins at `pending`). The SDK helper `compile_from_run` wraps the polling; see [`client.compilations`](/agents/sdk/python#clientcompilations) for the lower-level building blocks.
4. Point your production traffic at `run-instruction` with the new compilation id (step 2 above).

You can also freeze without touching REST at all: from any MCP client connected to the [FlyMyAI Agents MCP gateway](https://docs.flymy.ai/agents/guides/connect-your-agent.md), the `freeze_agent` and `run_frozen` tools do the same thing conversationally.

> **Note: A frozen endpoint never drifts**
>
> A frozen compilation replays a fixed instruction. A model update never changes a frozen endpoint - your product gets the same pipeline tomorrow that you approved today. When you want new behavior, you cut a new version deliberately: freeze a newer run, get a new compilation id, and switch your config when you are ready.

## Production notes

**Fire and forget from your hot path.** The only synchronous call in your request handler is the POST that starts the run - it returns the execution id in milliseconds. Persist the id, return to your user, and let a background worker poll every ~2 seconds until the run lands. Render `agent_result` when it does.

**Runs can pause for approval.** If the agent's goal contains approval rules ("ask me before sending anything external"), a run can pause mid-flight and raise an approval request in the app at [app.flymy.ai](https://app.flymy.ai); it resumes after the owner approves. From your product's side this is just a longer-running execution - keep polling and do not treat a slow run as a failure. See [Human-in-the-Loop Approvals](https://docs.flymy.ai/agents/guides/approvals.md).

**Cost is per run, per use.** There is no subscription - each run bills for what it actually used. The gateway tools `get_execution_price` and `list_execution_prices` (SDK: `client.runs.prices()`, `client.runs.list_prices()`) return what each run used and costs; a run's total is final once `billing_settled` is true, usually within about 10 minutes of its last call. As a reference point from real bills: a dictation-note pipeline like the example above lands at $0.031 per run.

**Schedules instead of on-demand.** If the same frozen run should fire on a schedule rather than per request, set a standard 5-field cron expression (plus an optional IANA timezone) on the compilation - `compilations.update` in the SDK or the `schedule_agent` gateway tool. The schedule fires per compilation, and each scheduled run appears as a normal execution you can poll like any other. See [scheduling in the SDK reference](https://docs.flymy.ai/agents/sdk/python.md#schedule-a-frozen-instruction-cron).

## Next steps

- **[Runs](https://docs.flymy.ai/agents/concepts/runs.md)** - lifecycle, event streaming, freeze semantics in depth
- **[Inputs, Outputs & Variables](https://docs.flymy.ai/agents/concepts/inputs.md)** - schemas that shape `variables` and `agent_result`
- **[Python SDK](https://docs.flymy.ai/agents/sdk/python.md)** - the same contract with polling, validation, and error handling built in
- **[Connect Claude & Any MCP Client](https://docs.flymy.ai/agents/guides/connect-your-agent.md)** - drive the same agents conversationally through the gateway
