> ## 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 latest bill

> Link several tasks together to fetch utility bills from credentials that contain many sub accounts.

![Fetched invoices shown as a list next to the structured JSON response](https://mintcdn.com/decksoftwareinc/dbrKq5gPOifVywqx/images/bill-fetch-hero.png?fit=max\&auto=format\&n=dbrKq5gPOifVywqx\&q=85\&s=fccadf3728f146d9f1915493635582e3)

Bill fetching can be a single task: log in and fetch the latest bill. When a credential holds many accounts, a single task returns one combined result, with no way to tell which accounts contributed, which are closed, or where a partial delivery came from.

This guide splits the work into three narrow tasks and chains them in a [workflow](https://docs.deck.co/concepts/workflows.md), so one run per credential discovers the accounts, checks each one, and fetches only the bills that are new:

1. **Extract accounts**: runs once per credential. Returns a list of accounts behind a single credential.
2. **Check for a new bill**: runs against a single account. Checks for the latest bill and reports whether a bill newer than your last known date exists.
3. **Fetch latest bill**: per-account download that delivers exactly one bill, with [extraction](https://docs.deck.co/guides/storage.md#document-extraction) returning its fields as structured data.

Splitting the bill fetch into several tasks gives you a few things:

* **Per-account attribution.** If a bill is missing for an account, there is one run to inspect, not a single long run across every account.
* **Retry granularity.** A failed run is tightly scoped and can be retried on its own.
* **Lower cost when nothing is new.** When there's no new bill, you pay for only the task run rather than deduplication and extraction.
* **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](https://docs.deck.co/guides/quickstart.md) covers those steps. The fetch task uses [storage and extraction](https://docs.deck.co/guides/storage.md), available as an add-on on paid plans. The [workflows guide](https://docs.deck.co/guides/workflows.md) covers the step types and run body used here.

## Create the tasks

All three tasks belong to the same agent. See [Tasks](https://docs.deck.co/concepts/tasks.md) for guidance on writing prompts.

**1. 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 active 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. If the source shows a latest bill date next to each account, include it.",
    "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" },
              "latest_bill_date": { "type": "string", "description": "Latest bill date shown on the account list (YYYY-MM-DD), null when the source does not display one." }
            }
          }
        }
      }
    }
  }'
```

Not every source shows a bill date on the account list, so treat `latest_bill_date` as a hint.

**2. Check for a new bill**

Runs once per account per checking cycle. It takes the account number and your last known bill date, and reports whether the source has anything newer. With storage disabled, runs of this task can't return files, so the task stays read-only regardless of prompt wording.

```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": "Check for a new bill",
    "agent_id": "agt_a1b2c3d4...",
    "prompt": "Log in and check whether account {account_number} has a bill dated after {last_known_bill_date}. Read the bill listing only; do not open or download any files. Return whether a newer bill exists and the newest bill date shown for the account.",
    "input_schema": {
      "type": "object",
      "required": ["account_number", "last_known_bill_date"],
      "properties": {
        "account_number": { "type": "string" },
        "last_known_bill_date": { "type": "string", "description": "Most recent bill date you have for this account (YYYY-MM-DD)." }
      }
    },
    "output_schema": {
      "type": "object",
      "required": ["account_number", "has_new_bill"],
      "properties": {
        "account_number": { "type": "string", "description": "Echoed from the input so downstream code can address the account." },
        "has_new_bill": { "type": "boolean" },
        "latest_available_bill_date": { "type": "string", "description": "Date of the newest bill shown for this account (YYYY-MM-DD), null when the source does not show one." }
      }
    }
  }'
```

**3. Fetch latest bill**

Runs only for accounts where the previous task returned `has_new_bill: true`. It takes the account number alone; the decision to fetch was already made upstream. Storage and extraction are enabled, and the extraction schema pulls the bill's fields from the file.

```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 latest bill",
    "agent_id": "agt_a1b2c3d4...",
    "prompt": "Log in and download the most recent bill for account {account_number}. Deliver exactly one file, the single most recent bill available for that account. Do not download older bills or bills for any other account.",
    "input_schema": {
      "type": "object",
      "required": ["account_number"],
      "properties": {
        "account_number": { "type": "string" }
      }
    },
    "output_schema": {
      "type": "object",
      "required": ["account_number", "bill_date"],
      "properties": {
        "account_number": { "type": "string", "description": "Echoed from the input." },
        "bill_date": { "type": "string", "description": "Date of the delivered bill (YYYY-MM-DD)." }
      }
    },
    "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 is optional here: the date input on the previous task already prevents most duplicate downloads. As an extra safeguard, enable [deduplication](https://docs.deck.co/guides/storage.md#deduplication) keyed on the statement number and account number.

## Create the workflow

One workflow runs all three tasks for a credential. Steps run in order in one session, so the agent logs in once. The two loops run 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": "Fetch latest bills",
    "steps": [
      {
        "name": "extract_accounts",
        "type": "task",
        "task_id": "task_extract..."
      },
      {
        "name": "check_each",
        "type": "for_each",
        "items": "steps.extract_accounts.output.accounts",
        "failure_behavior": "continue",
        "step": {
          "type": "task",
          "task_id": "task_check...",
          "input": { "account_number": "{{ item.account_number }}" }
        }
      },
      {
        "name": "fetch_new",
        "type": "for_each",
        "items": "steps.check_each.output",
        "failure_behavior": "continue",
        "step": {
          "type": "task",
          "task_id": "task_fetch...",
          "if": "item.has_new_bill",
          "input": { "account_number": "{{ item.account_number }}" }
        }
      }
    ]
  }'
```

`check_each` loops over the accounts the first step returned. Its definition sets only the account number; the cutoff date comes from the run body, below.

`fetch_new` loops over the check results, which is why the check task echoes `account_number` in its output. The `if` runs the fetch only where a newer bill exists. Accounts with no new bill get `null` in the step's output and no task run.

A failed check leaves `null` in `check_each`'s output. In the fetch loop, `item.has_new_bill` on `null` is false, so nothing is fetched for that account.

## Run the workflow

Run the workflow once per credential per cycle. Weekly is typical. Pass the credential on every step, and the cutoff date on `check_each`:

```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": "check_each",
        "credential_id": "cred_a1b2c3d4...",
        "input": { "last_known_bill_date": "2026-08-25" }
      },
      { "name": "fetch_new", "credential_id": "cred_a1b2c3d4..." }
    ]
  }'
```

The run-time `input` on `check_each` merges into every item's input alongside the account number, so one cutoff date applies to every account in the run. Use the date of your previous cycle. On the first run, set it far enough back that any bill counts as new. Always pass it: step input isn't checked against the task's schema before a run starts, so a run without the date sends `null` to every check and the results can't be trusted.

Before you advance the cutoff to this run's date, retry any account that failed, as a direct task run with the current cutoff. Once the cutoff moves forward, a bill that account missed is dated before it and won't count as new. See [Handle failures](#handle-failures).

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

## Read the results

Fetch the run with `include=storage` to get each downloaded bill and its extraction 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", "latest_bill_date": "2026-08-18" },
          { "account_number": "58291-44738", "latest_bill_date": null }
        ]
      },
      "task_runs": [ { "task_run_id": "trun_a1b2c3d4...", "status": "completed", "result": "success" } ]
    },
    {
      "name": "check_each",
      "type": "for_each",
      "status": "completed",
      "output": [
        { "account_number": "58291-44720", "has_new_bill": true, "latest_available_bill_date": "2026-08-18" },
        { "account_number": "58291-44738", "has_new_bill": false, "latest_available_bill_date": "2026-07-20" }
      ],
      "task_runs": [
        { "task_run_id": "trun_e5f6g7h8...", "status": "completed", "result": "success", "item": { "account_number": "58291-44720", "latest_bill_date": "2026-08-18" } },
        { "task_run_id": "trun_i9j0k1l2...", "status": "completed", "result": "success", "item": { "account_number": "58291-44738", "latest_bill_date": null } }
      ]
    },
    {
      "name": "fetch_new",
      "type": "for_each",
      "status": "completed",
      "output": [
        { "account_number": "58291-44720", "bill_date": "2026-08-18" },
        null
      ],
      "task_runs": [
        {
          "task_run_id": "trun_m3n4o5p6...",
          "status": "completed",
          "result": "success",
          "item": { "account_number": "58291-44720", "has_new_bill": true, "latest_available_bill_date": "2026-08-18" },
          "storage": [
            {
              "id": "stor_x1y2z3...",
              "object": "storage",
              "file_name": "statement_aug_2026.pdf",
              "file_type": "application/pdf",
              "url": "https://files.deck.co/stor_x1y2z3...?signature=...",
              "extraction": {
                "account_number": "58291-44720",
                "bill_date": "2026-08-18",
                "due_date": "2026-09-11",
                "total_amount": 6925.18
              },
              "purpose": "output"
            }
          ]
        }
      ]
    }
  ]
}
```

Each loop's `output` has one slot per account, in the order the first step returned them. In `fetch_new`, a `null` slot is an account with no new bill. Each `task_runs` entry names the account it processed in `item`, and with `include=storage` carries the 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`. You don't need a date per account; the next run's cutoff is the date of this one.

## Schedule the work

A [trigger](https://docs.deck.co/guides/triggers.md#targeting-a-workflow) runs the workflow on a schedule for every credential on the source. Each fire creates one workflow run per credential, and the trigger's credential is used by every step, so `steps` only needs the cutoff:

```bash Request theme={null}
curl -X POST https://api.deck.co/v2/triggers \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekly bill fetch",
    "condition": {
      "type": "schedule",
      "cron": "0 6 * * 1",
      "timezone": "America/New_York"
    },
    "workflow_id": "wflo_a1b2c3...",
    "credential_filter": {
      "source_ids": ["src_utility..."],
      "status": ["unverified", "verified"]
    },
    "steps": [
      {
        "name": "check_each",
        "input": { "last_known_bill_date": "2026-08-25" }
      }
    ],
    "concurrency_max": 50
  }'
```

The trigger's `steps` config is fixed between fires, so advance the cutoff yourself before each one: after a cycle's runs finish and any failed accounts are retried, update the trigger with the new date.

```bash Request theme={null}
curl -X PATCH https://api.deck.co/v2/triggers/trg_a1b2c3... \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "steps": [
      {
        "name": "check_each",
        "input": { "last_known_bill_date": "2026-09-01" }
      }
    ]
  }'
```

`concurrency_max` caps how many workflow runs the trigger has in flight at once. Each run holds one [session](https://docs.deck.co/concepts/sessions.md) until it finishes, so set the cap within your organization's session limit.

If you'd rather run the workflow from your own scheduler, list the credentials on the source with `GET /v2/credentials?source_id=src_...&status=verified,unverified` and start a run for each, passing the cutoff on the run body. A run requested over the session limit is rejected with `session_limit_exceeded` rather than queued.

## 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 loops, 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](https://docs.deck.co/api/errors.md).

Retry each failed account directly, with the credential, the account number, and the cutoff you used for the run, before you advance the cutoff. The workflow doesn't need to run again.

* **The fetch failed.** The check already reported a new bill for this account. Run the task that fetches the latest bill for it.
* **The check failed.** Run the task that checks for a new bill for it with the same cutoff, then fetch if it reports one.

Once every failed account is retried, advance the cutoff to this run's date.

If the credential goes `invalid` (a changed password, for example), Deck emits a `credential.invalid` [event](https://docs.deck.co/events/events.md) so you can prompt the user to re-authenticate. A trigger drops the credential from its scope on its own and picks it back up once the user re-authenticates. Its first run afterward uses the trigger's current cutoff, so run the workflow directly for that credential with the cutoff from its last successful run if bills may have landed in between.


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