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

# Jobs and results

> Wait for a job or run it in the background, follow its status, cancel it, and know how long its result is kept.

Every call to Parse, Extract, Split, Tables, Charts and Classify starts a job. A job's id names its endpoint, such as `parse-3f6c2a9e-…` or `extract-9b1d4e7a-…`. Every job has three routes, and so do [pipelines](https://docs.runpulse.com/v2/pipelines.md) and [batches](https://docs.runpulse.com/v2/batch.md):

| Route | What it does |
| - | - |
| `GET /{endpoint}/{id}` | Returns the job's status. |
| `GET /{endpoint}/{id}/result` | Returns the job's result. |
| `DELETE /{endpoint}/{id}` | Cancels the job. |

***

## Sync or async

By default, a `POST` waits for its job to end. It returns `200` with the job's id when the job completes, or an error status when the job fails. Send `"async": true` to get `202` with the id at once. Pipelines and batches always run async.

Use async for long documents, and wherever a client, proxy or load balancer might time out before the job ends. Either way, you read the result from the job's `/result` route.

This is an async parse, followed until it ends:

```mermaid theme={null}
%%{init: {"themeVariables": {"fontSize": "14px"}, "sequence": {"mirrorActors": false, "messageMargin": 28, "height": 44, "boxMargin": 6}}}%%
sequenceDiagram
    participant App
    participant Pulse
    App->>Pulse: POST /parse, async: true
    Pulse-->>App: 202 with parse_id
    App->>Pulse: GET /parse/{parse_id} until it ends
    App->>Pulse: GET /parse/{parse_id}/result
```

Instead of polling, you can have a [webhook](https://docs.runpulse.com/v2/webhooks.md) tell your server when the job ends.

***

## Statuses

| Status | Meaning |
| - | - |
| `queued` | Accepted and waiting to start. |
| `processing` | Running. |
| `completed` | Finished, with its result ready. |
| `failed` | Stopped with an error. The status read returns `200`, with the reason in `error`. |
| `canceled` | Canceled with `DELETE` before it ended. |
| `expired` | Ended, and its result has since been deleted. See [Retention](#retention). |

This diagram shows how a job moves between statuses:

```mermaid theme={null}
%%{init: {"themeVariables": {"fontSize": "14px"}, "flowchart": {"nodeSpacing": 24, "rankSpacing": 32, "padding": 8}}}%%
stateDiagram-v2
    direction LR
    [*] --> queued: async
    [*] --> processing: sync
    queued --> processing
    processing --> completed
    processing --> failed
    processing --> canceled: DELETE
    queued --> failed
    queued --> canceled: DELETE
    completed --> expired: deleted
    failed --> expired: deleted
    canceled --> expired: deleted
```

***

## Wait for a job

This helper polls a job until it ends. Pass it the status call for the job's endpoint, such as `client.extract.get`. `client` is the one from [SDKs](https://docs.runpulse.com/v2/sdks.md).

```python Python theme={null}
import time

def wait(get, job_id):
    """Poll a job until it ends, waiting longer between reads, up to 15 seconds."""
    delay = 2.0
    while (status := get(job_id)).status in ("queued", "processing"):
        time.sleep(delay)
        delay = min(delay * 1.5, 15)
    return status

job = client.parse.run(file_url="https://example.com/report.pdf", config={"model": "ultra-2"}, async_=True)
if wait(client.parse.get, job.parse_id).status == "completed":
    result = client.parse.get_result(job.parse_id)
```

```typescript TypeScript theme={null}
async function wait<S extends { status: string }>(get: () => Promise<S>): Promise<S> {
  for (let delay = 2000; ; delay = Math.min(delay * 1.5, 15000)) {
    const status = await get();
    if (status.status !== "queued" && status.status !== "processing") return status;
    await new Promise((r) => setTimeout(r, delay));
  }
}

const { parseId } = await client.parse.run({ fileUrl: "https://example.com/report.pdf", config: { model: "ultra-2" }, async: true });
if ((await wait(() => client.parse.get({ parseId }))).status === "completed") {
  const result = await client.parse.getResult({ parseId });
}
```

Status reads are free. The SDKs don't retry for you, so retry a failed read in your own code, as shown in [Errors and retries](https://docs.runpulse.com/v2/errors.md).

***

## Read the result

`GET /{endpoint}/{id}/result` returns the result only for a `completed` job. For any other status, it returns:

| Job status | `/result` returns |
| - | - |
| `queued` or `processing` | `409`. Wait for the job to end. |
| `failed` | The job's error. |
| `canceled` or `expired` | `410`. The result is gone; run the job again. |

Anyone in your organization can read a job's status and result.

***

## Cancel a job

`DELETE /{endpoint}/{id}` cancels a job that's `queued` or `processing`. It returns the job's status, now `canceled`, and you aren't billed for the job. A job that has already ended returns `409`.

Only the user who started a job or pipeline can cancel it. For anyone else, it isn't found (`404`). Anyone in your organization can cancel a batch.

`DELETE` never deletes a finished job's result, and no route does. A result is deleted only when it expires, as described next.

***

## Retention

By default, Pulse keeps every result and it has no `expires_at`. These cases make results expire:

| Case | What expires | When |
| - | - | - |
| You send a parse with `config.storage.save: false` | That parse's result | 24 hours later |
| Your organization has turned saving off | Every Parse, Extract, Split and Tables result, and every pipeline result | 24 hours later |
| Your organization has results expire after 1 hour or 24 hours | Every result, unsaved parses included | 1 hour or 24 hours later |
| Your organization deletes results on first read | Each result | When it's first read, or when it expires if that comes first |

The clock starts when Pulse accepts the request, except for Extract, whose clock starts when the job ends. A batch's jobs and a pipeline's steps count from when the batch or pipeline ends. A completed job's `expires_at` gives the exact time.

An expiry of 1 hour or 24 hours replaces both 24-hour rules. No setting keeps a parse sent with `save: false` for more than 24 hours. Your organization's retention is set by Pulse; to change it, contact [support](https://docs.runpulse.com/v2/support.md).

A step sent an expired parse's `parse_id` is refused.

### Where results are deleted on first read

If your organization deletes each result the first time it's read, the first `GET …/result` returns the result and deletes it. A second read returns `410`. A step that reads the parse after that fails or is refused, even one you started earlier.

> [!WARNING]
>
> Start every step you need on a parse, and wait for each to end, before you read the parse's own result. Or run the steps in a [pipeline](https://docs.runpulse.com/v2/pipelines.md). Store each result you read, because you can't read it again.

This is the safe order for a parse you also run Extract on:

```mermaid theme={null}
%%{init: {"themeVariables": {"fontSize": "14px"}, "sequence": {"mirrorActors": false, "messageMargin": 28, "height": 44, "boxMargin": 6}}}%%
sequenceDiagram
    participant App
    participant Pulse
    App->>Pulse: POST /extract with parse_id
    Pulse-->>App: 200 when the extract ends
    App->>Pulse: GET /parse/{parse_id}/result
    Pulse-->>App: 200, then the parse is deleted
```


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