---
title: Background jobs and retries
description: Pick the right route for an agent call, run long work as a background job, and retry without running a turn twice.
updated: 2026-10-29
---

> All VIABLE docs, for people and their agents: https://developers.viablelab.org/llms.txt

# Background jobs and retries

Every published agent answers on three routes. Pick the one that fits the work, and retry an unanswered request safely.

## Choose a route

All three run the same agent and share keys, `user` and `learner_group`.

| | `/runs` | `/jobs` | `/chat/completions` |
| --- | --- | --- | --- |
| You send | `input`, optional `conversation_id` | The same, plus `Idempotency-Key` | `model` (the slug) and `messages` |
| You get | The result, or a stream | `202` and a job to poll | `choices[0].message.content` |
| Time | 60 s, or the agent's 10–120 | 5 minutes or more | As `/runs` |
| In the dashboard | API · runs | API · background job | API · chat completions |

Start with `/runs` when someone is waiting; `"stream": true` streams the reply. Use `/jobs` for long research, image generation, or a host that cuts requests short. For OpenAI-compatible SDKs, set the base URL to `https://api.viablelab.org/v1/agents/{slug}`; agents with Ask user steps need `/runs`.

## Run a background job

```bash
curl https://api.viablelab.org/v1/agents/YOUR_AGENT_SLUG/jobs \
  -H "Authorization: Bearer $VIABLE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-app-turn-8f2c-0004" \
  -d '{"input": "Draft a reading list on photosynthesis.", "user": "my-app:student-0001"}'

# Then poll its status_url with the same key:
curl https://api.viablelab.org/v1/agents/YOUR_AGENT_SLUG/jobs/agtj_... \
  -H "Authorization: Bearer $VIABLE_API_KEY"
```

The `202` has an `id` (`agtj_…`), a `status_url` and a `Retry-After` header. Wait that long between polls.

- `queued` or `running`: poll again.
- `completed`: `result` is the body `/runs` returns.
- `failed`: `error` has the `code`. To try again, submit a new job with a new key. A job not started within 15 minutes fails with `agent_job_expired`, and one that stopped before its run began with `agent_job_interrupted`: nothing ran, so send the turn again.
- `recovery_required`: the run had started, and completion is uncertain: it may have drawn an image or called a web address. Check Run history; don't submit the turn again.

Keep one job per conversation at a time.

## Retry safely

Send an `Idempotency-Key` with every turn: 8–128 letters, digits, dots, underscores, colons or hyphens. Save the route, body and key on your server. If no answer arrives, resend the same body to the same route with the same key.

- **The run completed:** you get the same result.
- **Still going:** 409 `agent_request_pending`. Retry later with the same key.
- **The body differs:** 409 `idempotency_conflict`.
- **It started, then failed** (such as `provider_timeout`): the key keeps that error. Retry with a new key.
- **Rejected before it started** (validation, 401, 403, 404, 413, rate or spending limits): nothing was recorded. Reuse the key once fixed.

> [!WARNING]
> Never change the key to get past a pending or uncertain turn: that can run it twice. Never switch a pending turn between `/runs` and `/jobs`.

## Limits and errors

- **Size:** 12 MiB per request, `context` up to 128 KiB, up to 400 `messages`.
- **Rate:** a key's per-minute limit covers all your users: thirty learners in one class share it.
- **429:** wait for `Retry-After`. For token, cost or credit limits, don't retry in a loop.

Branch on `error.code`, and never show an error as the agent's reply. The [API reference](/docs/api) lists every code.

::: cards
- [Learners and traces](/docs/connect/learners-and-traces): keep each turn's run ID and trace.
- [Keys, credit and costs](/docs/project/keys-and-costs): set a key's limits and see who pays.
:::
