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

# Jobs

> Run long capabilities asynchronously and wait for the result.

Some capabilities take minutes, for example `web.research`, `web.crawl`, `video.generate` and
`browser.task.run`. Their reference page says **Mode: async**. Calling one starts a job:

```bash theme={null}
curl https://api.pramaan.dev/v1/capabilities/web.research \
  -H "Authorization: Bearer $PRAMAAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "How do open source CRMs make money?"}'
```

```json theme={null}
HTTP/1.1 202 Accepted
{ "job_id": "job_01J...", "status": "running", "held_micro": 130000 }
```

## Wait for a job

Poll `GET /v1/jobs/{id}` until `status` is no longer `running`:

| Status | Meaning |
| - | - |
| `running` | Still working. Poll again in 2 seconds or more. |
| `done` | Finished. `result` holds the capability result. |
| `failed` | Stopped with `error` (`code`, `message`). |
| `cancelled` | You cancelled it with `POST /v1/jobs/{id}/cancel`. |

The SDK does the polling for you, starting at 2 seconds and backing off to 30:

```ts theme={null}
const job = await p.run("web.research", { query: "How do open source CRMs make money?" });
const done = await p.jobs.wait(job.id, { timeoutMs: 600_000 });
if (done.status === "done") console.log(done.result);

// Or in one step: call() waits and throws if the job fails.
const result = await p.call("web.research", { query: "..." });
```

Instead of polling, you can subscribe to the `job.completed` [webhook](https://docs.pramaan.dev/guides/webhooks.md).

## Bulk runs

A capability whose id ends in `-bulk` runs one lookup for many rows as one job. Give every row
your own `id`; the result has one entry per row, in the order sent, keyed by that id:

```json theme={null}
{
  "results": [
    { "id": "row-1", "found": true, "email": "jane@example.com" },
    { "id": "row-2", "found": false },
    { "id": "row-3", "found": false, "error": { "code": "upstream_unavailable", "message": "..." } }
  ],
  "summary": { "total": 3, "found": 1, "notFound": 1, "failed": 1 }
}
```

* `found: false` with no `error` means there is no record for that row. With an `error` the
  row could not be answered: it is free, and you can send it again later.
* While the job runs, `GET /v1/jobs/{id}` may carry `progress: { "total": 3, "done": 1 }`.
* If you cancel, the rows already answered stay on the job as its `result`, and those are the
  rows you pay for.
* A run in which no row could be answered fails and is free.

## Limits and charges

* A job that runs longer than 30 minutes fails with `upstream_timeout`. A few capabilities allow
  longer, up to 24 hours.
* To see what a job would cost before starting it, ask for an
  [estimate](https://docs.pramaan.dev/guides/pricing.md).
* Before a job starts, the most it can cost is set aside from your credit (a hold). If your
  balance or your daily spend cap cannot cover it, the request is refused with `402
  spend_cap_reached` and nothing runs. The error's `reason` is `balance` (top up) or `daily_cap`
  (raise your daily cap), and `required_micro` is the amount needed. The hold is not a charge:
  when the job ends you pay its real price and the rest comes back. `GET /v1/balance` shows what
  is currently set aside as `held_micro`.
* The start answer carries `held_micro`: the credit set aside for that job (0 when it takes
  none). It is never a price — the price is not known until the job is settled.
* A job view answers with its own money. `charged_micro` is what the job has been charged so
  far: 0 while it runs, its price once it is settled, and 0 again for a job that failed or was
  refused and charged nothing. `held_micro` is what is still set aside for it: its estimate
  while it runs, 0 once it has finished. That is one job's own charge, which the per-day,
  per-capability usage totals do not break down. A long outcome run keeps
  its money in its result's `cost`, and its job view carries neither field.
* A job that finishes with `done` is charged its real price, and what was set aside comes
  back. Where the data source stated its worst case for the request up front, the price is
  never more than the amount set aside. A failed job is free.
* A job you cancel is charged for the work already done, where there was any, and never more
  than the amount set aside. Cancelling before any work was done is free.
* Results are kept for 7 days.
* Sending the same `Idempotency-Key` again returns the same `job_id` instead of starting a new
  job (see [idempotency](https://docs.pramaan.dev/guides/idempotency.md)).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.