Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 5 Nov 2026. What's built · Install 0.3.0
jadpo
ReferenceErrors

ErrorsBackground work · rule

step.context

On this page

An action makes several changing calls from somewhere a retry can't carry on where it stopped, such as a route whose API doesn't require Idempotency-Key.

Area 12, Background work; not declinable; the next step is usually the human's.

Why Jadpo refuses it

A changing call, such as a store's put or a provider operation with changes: true, takes effect as soon as it is made. When an action makes several, Jadpo records each one under the action's key, so a retry replays the calls already answered instead of making them again. That only works when the retry has the same key as the first try.

A subscriber's delivery keeps its key across retries, and so does a route whose API requires the client's Idempotency-Key. A route of an API that doesn't require it gets a new key each time its client tries again, so a retry would make every call a second time. Jadpo won't choose between the fixes for you, because each changes what the route's clients see.

What it looks like

domains/bakery/routes/price_sheets.jadpo, in a bakery app whose file_prices keeps a supplier's price sheet in sheets, a folder of files, and then a copy for the accountant:

example
use app { sheets }use bakery/types { PriceSheetUpload }use bakery/failures { SheetNotKept }/// Keeps a supplier's price sheet, then a copy for the accountant.action file_prices(sent_sheet: PriceSheetUpload) fails SheetNotKept -> Unit {    var sheet: File = attempt sheets.put(encoding.to_utf8(sent_sheet.sheet_text), FileType.csv) {        storage_full: SheetNotKept    }    var invoice_copy: File = attempt sheets.put(        encoding.to_utf8(sent_sheet.sheet_text),        FileType.csv    ) {        storage_full: SheetNotKept    }    assert sheet != invoice_copy}
example
use bakery/types { PriceSheetUpload }use bakery/actions { file_prices }/// Where suppliers send their price sheets.routes PriceSheets {    prefix: /price-sheets    /// Keeps a supplier's price sheet.    POST / {        authentication: RouteAuthentication.none        input: PriceSheetUpload        status: SuccessStatus.no_content        run: file_prices(input)    }}
jadpo checkcheck failed: 1 error, 8 files

error POST /price-sheets runs file_prices, which makes 2 changing calls, but API main doesn't require Idempotency-Key

--> domains/bakery/routes/price_sheets.jadpo:9:5

  |9 |     POST / {  |     ^^^^^^ a retry can't resume them
reason
Several changing calls are each recorded under their action's key, so a retry carries on from the last answer instead of sending them again; a route keeps its key across a retry only when its API requires the client's Idempotency-Key.
next
Require the key with idempotency: Idempotency.required on API main, or answer 202 and make the calls in a subscriber (human)
rule
step.context
help
https://jadpo.dev/docs/errors/step.context

(human) at the end of the next: line tells your coding agent to stop and ask you which fix you want, because the choice is yours to make: it decides whether clients must send Idempotency-Key or wait for the work in the background.

A schedule's retry is its next run, under a key of its own, and a run keeps each item to one changing call. So a schedule, and a batches.each whose action makes several changing calls, are refused the same way, at the schedule and at the run's call.

How to fix it

The question is what the route's clients should do.

If clients can send Idempotency-Key, require it on API main in app.jadpo. This app writes no database yet, and the record of the calls needs one (step.store_required), so the fix also connects main and names it in events: { store: }:

example
        main: {            driver: Driver.postgres            url: config.main_url        }    apis: {        main: {            prefix: /            idempotency: Idempotency.required        }    }    events: {        store: main    }

A request to POST /price-sheets without the header now answers 400 invalid_request, and the API's OpenAPI document lists the header. A client that retries with the same key gets the first try's answer, and file_prices's calls are replayed rather than made twice.

If clients shouldn't wait for the work, have the route answer 202 and make the calls in a subscriber. The route runs a new action, receive_prices, in domains/bakery/routes/price_sheets.jadpo:

example
        output: SheetReceipt        status: SuccessStatus.accepted        run: receive_prices(input)

receive_prices announces the sheet with an event, PriceSheetSent, in domains/bakery/actions/receive_prices.jadpo:

example
action receive_prices(sent_sheet: PriceSheetUpload) -> SheetReceipt {    emit PriceSheetSent {        sheet_text: sent_sheet.sheet_text    }    ...}

A subscriber runs file_prices for each one, in domains/bakery/subscribers/file_sent_prices.jadpo, and file_prices now takes the event:

example
use bakery/actions { file_prices }use bakery/events { PriceSheetSent }/// Files each price sheet a supplier sends.subscriber file_sent_prices {    on: PriceSheetSent    run: file_prices(event)}

The route now answers 202 with a SheetReceipt once the event is saved. The two sheets.put calls happen in the subscriber's delivery, which keeps its key when it is tried again. The event waits in main, which events: { store: } names, since receive_prices writes no database. An event's JSON is at most 64 KB, so this suits sheets up to that size.

Markdown for agents: /docs/errors/step.context.md.Built from Jadpo main at aee2e8f.