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

# Fetch historical bills

> Link two tasks together to backfill historical utility bills from credentials that hold many accounts.

Backfilling bill history can be a single task: log in and fetch every bill. When a credential holds many accounts, a single task returns one combined result, with no way to tell which accounts contributed, which histories are complete, or which run to investigate when a bill is missing.

This guide splits the work into two narrow tasks and chains them in a [workflow](/concepts/workflows), so one run per credential discovers the accounts and backfills each one:

1. **Extract accounts**: runs once per credential. Returns a list of accounts behind a single credential.
2. **Fetch bill history**: runs against a single account. Downloads every bill for that account, optionally bounded by a start date.

Splitting the backfill into two tasks gives you a few things:

* **Per-account attribution.** If a bill is missing for an account, there is one run to inspect.
* **Retry granularity.** A failed run is tightly scoped and can be retried on its own.
* **Bounded runs.** One account's history is a bounded unit of work, so runtime and file counts stay predictable.
* **Resumable backfill.** The `since_bill_date` input picks up where a partial backfill stopped, without re-fetching bills you already have.
* **A durable account list.** Every run returns the current account list. If an account disappears from a later run, you notice.
* **One login per credential.** The workflow runs every step in one session, so the agent signs in once and works through the accounts.

## Prerequisites

This guide assumes you have an agent, a source, and a stored credential; the [Quickstart](/guides/quickstart) covers those steps. The history task uses [storage, extraction, and deduplication](/guides/storage), available as an add-on on paid plans. The [workflows guide](/guides/workflows) covers the step types and run body used here.

## Create the tasks

Both tasks belong to the same agent. See [Tasks](/concepts/tasks) for guidance on writing prompts.

<Steps>
  <Step title="Extract accounts">
    Runs once per credential per refresh cycle, typically monthly, or on demand when a user adds or removes accounts on the source. You store the returned list. The task returns metadata only, so leave storage off.

    ```bash Request theme={null}
    curl -X POST https://api.deck.co/v2/tasks \
      -H "Authorization: Bearer sk_live_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Extract accounts",
        "agent_id": "agt_a1b2c3d4...",
        "prompt": "Log in and return the list of accounts visible on the source. For each account, return the account number exactly as displayed, preserving hyphens, spaces, and leading zeros, not a nickname, service address, or any other identifier. Include the service type and whether the account is active, where the source shows them.",
        "input_schema": {
          "type": "object",
          "properties": {}
        },
        "output_schema": {
          "type": "object",
          "required": ["accounts"],
          "properties": {
            "accounts": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["account_number"],
                "properties": {
                  "account_number": { "type": "string" },
                  "service_type": { "type": "string", "description": "Service type shown on the source (electricity, gas, water), null when not displayed." },
                  "is_active": { "type": "boolean" }
                }
              }
            }
          }
        }
      }'
    ```

    Not every source labels service type or account status, so treat both as hints.
  </Step>

  <Step title="Fetch bill history">
    Runs once per account. It takes the account number and an optional `since_bill_date` that bounds the fetch, used to resume a partial backfill or recover a gap. Storage, extraction, and deduplication are enabled: extraction pulls each bill's fields from the file, and deduplication skips bills a previous run already captured.

    ```bash Request theme={null}
    curl -X POST https://api.deck.co/v2/tasks \
      -H "Authorization: Bearer sk_live_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Fetch bill history",
        "agent_id": "agt_a1b2c3d4...",
        "prompt": "Log in and download every bill available for account {account_number}. If {since_bill_date} is provided, only download bills dated after {since_bill_date}. Deliver one file per bill. Do not download the same bill twice or bills for any other account.",
        "input_schema": {
          "type": "object",
          "required": ["account_number"],
          "properties": {
            "account_number": { "type": "string" },
            "since_bill_date": { "type": "string", "description": "Only bills dated after this date (YYYY-MM-DD). Omit to fetch the full history." }
          }
        },
        "output_schema": {
          "type": "object",
          "required": ["account_number", "bills"],
          "properties": {
            "account_number": { "type": "string", "description": "Echoed from the input." },
            "bills": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["bill_date"],
                "properties": {
                  "bill_date": { "type": "string", "description": "Bill date as shown on the listing (YYYY-MM-DD)." },
                  "bill_amount": { "type": "string", "description": "Amount shown on the bill listing, null when the source does not display one." }
                }
              }
            }
          }
        },
        "storage": {
          "enabled": true,
          "extraction": true,
          "extraction_schema": {
            "type": "object",
            "properties": {
              "account_number": { "type": "string", "description": "Account number printed on the bill" },
              "bill_date": { "type": "string", "format": "date", "description": "Bill issue date" },
              "due_date": { "type": "string", "format": "date", "description": "Payment due date" },
              "total_amount": { "type": "number", "description": "Total amount due, including tax" }
            }
          },
          "deduplication": true,
          "deduplication_schema": {
            "type": "object",
            "properties": {
              "statement_number": {
                "type": "string",
                "description": "The bill number printed on the document, under a label like Bill #, Invoice Number, or Statement Number. Only a printed, labeled identifier, never a barcode, batch, or file identifier."
              },
              "account_number": {
                "type": "string",
                "description": "The utility account number, exactly as printed"
              }
            }
          }
        }
      }'
    ```

    The captured files appear in [storage](/guides/storage), not the output. The `bills` array carries the dates the agent read from the listing, which is what you use to update your stored state.
  </Step>
</Steps>

## Create the workflow

One workflow runs both tasks for a credential. Steps run in order in one session, so the agent logs in once. The loop runs under `failure_behavior: "continue"`, so one account's failure doesn't stop the rest.

```bash Request theme={null}
curl -X POST https://api.deck.co/v2/workflows \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Backfill bill history",
    "steps": [
      {
        "name": "extract_accounts",
        "type": "task",
        "task_id": "task_extract..."
      },
      {
        "name": "fetch_history",
        "type": "for_each",
        "items": "steps.extract_accounts.output.accounts",
        "failure_behavior": "continue",
        "step": {
          "type": "task",
          "task_id": "task_history...",
          "if": "item.is_active != false",
          "input": { "account_number": "{{ item.account_number }}" }
        }
      }
    ]
  }'
```

`fetch_history` loops over the accounts the first step returned. The `if` skips accounts the source marks closed. An account with no status still runs, since `is_active` is only a hint. Skipped accounts get `null` in the step's output and no task run.

The definition sets only the account number. `since_bill_date` comes from the run body when you need it, below.

## Run the workflow

Run the workflow once per credential. Pass the credential on every step. On the first backfill, leave `since_bill_date` out so each account fetches its full history:

```bash Request theme={null}
curl -X POST https://api.deck.co/v2/workflows/wflo_a1b2c3.../run \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "steps": [
      { "name": "extract_accounts", "credential_id": "cred_a1b2c3d4..." },
      { "name": "fetch_history", "credential_id": "cred_a1b2c3d4..." }
    ]
  }'
```

On later runs, add `since_bill_date` to `fetch_history`. The run-time `input` merges into every item's input alongside the account number, so one date applies to every account in the run:

```json theme={null}
{
  "name": "fetch_history",
  "credential_id": "cred_a1b2c3d4...",
  "input": { "since_bill_date": "2026-07-01" }
}
```

The response is a workflow run in `queued`. Runs are asynchronous: subscribe to `workflow_run.completed` and `workflow_run.failed` [events](/events/events), or poll the run.

Each run holds one [session](/concepts/sessions) until it finishes, so when backfilling many credentials, pace the starts to stay within your organization's session limit. A run requested over the limit is rejected with `session_limit_exceeded` rather than queued. To backfill every credential on a source without writing the loop yourself, point a [trigger](/guides/triggers#targeting-a-workflow) at the workflow with a `concurrency_max` inside that limit, then deactivate it once the fire completes.

## Read the results

Fetch the run with `include=storage` to get each account's bills and their extractions inline:

```text theme={null}
GET /v2/workflow-runs/{workflow_run_id}?include=storage
```

```json Response theme={null}
{
  "id": "wrun_a1b2c3...",
  "object": "workflow_run",
  "status": "completed",
  "result": "success",
  "steps": [
    {
      "name": "extract_accounts",
      "type": "task",
      "status": "completed",
      "output": {
        "accounts": [
          { "account_number": "58291-44720", "service_type": "electricity", "is_active": true },
          { "account_number": "58291-44738", "service_type": null, "is_active": true }
        ]
      },
      "task_runs": [ { "task_run_id": "trun_a1b2c3d4...", "status": "completed", "result": "success" } ]
    },
    {
      "name": "fetch_history",
      "type": "for_each",
      "status": "completed",
      "output": [
        {
          "account_number": "58291-44720",
          "bills": [
            { "bill_date": "2026-07-22", "bill_amount": "6925.18" },
            { "bill_date": "2026-06-20", "bill_amount": "7102.44" }
          ]
        },
        {
          "account_number": "58291-44738",
          "bills": [
            { "bill_date": "2026-07-19", "bill_amount": "312.40" }
          ]
        }
      ],
      "task_runs": [
        {
          "task_run_id": "trun_e5f6g7h8...",
          "status": "completed",
          "result": "success",
          "item": { "account_number": "58291-44720", "service_type": "electricity", "is_active": true },
          "storage": [
            {
              "id": "stor_x1y2z3...",
              "object": "storage",
              "file_name": "statement_jul_2026.pdf",
              "file_type": "application/pdf",
              "url": "https://files.deck.co/stor_x1y2z3...?signature=...",
              "extraction": {
                "account_number": "58291-44720",
                "bill_date": "2026-07-22",
                "due_date": "2026-08-15",
                "total_amount": 6925.18
              },
              "purpose": "output"
            }
          ]
        },
        {
          "task_run_id": "trun_i9j0k1l2...",
          "status": "completed",
          "result": "success",
          "item": { "account_number": "58291-44738", "service_type": null, "is_active": true },
          "storage": [ ... ]
        }
      ]
    }
  ]
}
```

The loop's `output` has one slot per account, in the order the first step returned them. Each `task_runs` entry names the account it processed in `item`, and with `include=storage` carries every bill it delivered. For anything beyond that, fetch the task run itself with `GET /v2/task-runs/{task_run_id}`.

Store the account list from `extract_accounts` and the date you ran the backfill. On the next run, set `since_bill_date` to the day before that date. A bill issued on the run date can land on either side of the cutoff, and deduplication skips any bill the overlap picks up again.

## Bounding the backfill

For sources with years of history, walk the history in windows instead of one large run: start with `since_bill_date` a few months back, then run again with the date further back each time. One date applies to every account in a run, so each run is one window across the whole credential. Deduplication skips bills already captured, so overlapping windows don't produce duplicate files, and each run's file count and runtime stay predictable.

To recover a delivery gap on specific accounts, run the task that fetches bill history directly for just those accounts, with `since_bill_date` set to the newest bill you have. The workflow doesn't need to run.

## Handle failures

If `extract_accounts` fails, the run ends `failed` with the step named in its `errors`, and nothing else runs. Fix the cause and run the workflow again.

Inside the loop, a failed account doesn't stop the others. The run finishes `completed` with `result: "failure"`, the account's slot in the step's `output` is `null`, and its `task_runs` entry carries the `errors`. See [Errors](/api/errors). To retry that account alone, run the history task directly with the credential and account number, without re-running the workflow.

If the credential goes `invalid` (a changed password, for example), Deck emits a `credential.invalid` [event](/events/events) so you can prompt the user to re-authenticate. Skip that credential until it recovers, then run the workflow with `since_bill_date` set to the day before its last successful run.


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