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
ReferenceUsing packages

External servicesUsing packages

What package code may do

On this page

A provider package may act on its own API, but never on your app's data. That way a package can carry what someone who knows the API has learned, written once and tested, without reaching into the apps that install it.

  • No state and no database. A package owns no entities or tables and never reads or writes the app's data. Data a package fetches reaches the app as a return value or an event, and the app's own action writes it.
  • Package actions call only their own API. A provider package may hold public actions in actions/. They call the package's own operations in any order, pass answers between them, use its pure functions, and return a result or fail. They can't emit app events, send email, call any other connection, or read configuration or secrets. The app calls one as connection.action(input) inside its own action. One with several changing calls follows Several changing calls through a package. Inside a package, operations are named without the connection: customers.create.
  • Package subscribers on its own webhooks. A package may subscribe to its own webhook events, call its own API to enrich them, and emit its own declared events, such as Stripe.SubscriptionRenewed.
  • Events only about the outside world. A package emits events for webhooks, enriched webhooks and polls, never as a side effect of the app calling it. Where the app wants an event after a call, the app's own action emits it, where a reviewer sees it. So an action that emits can't be public: only the package's subscribers and polls run it. Package events travel through the app's outbox like any other.
  • Package code runs only when started: by the app calling it, or by the outside world through a webhook or a poll the app subscribes to.

A package's API can also be a container Jadpo runs, which isn't built yet: Running a service in a container.

In this sketch, Stripe's package holds an action that makes several calls in the order Stripe requires. Its subscriber renewal hears the raw InvoicePaid webhook event, which isn't public, and runs announce_renewal, which emits the public SubscriptionRenewed. The app calls the action through its stripe connection:

packages/stripe/v2024_06_20/actions/start_subscription.jadpo
// packages/stripe/v2024_06_20/actions/start_subscription.jadpo/// Creates the customer, attaches the card, starts the subscription and/// finalises its first invoice, in the order Stripe requires.public action start_subscription(input: StartSubscription) fails CardDeclined -> StartedSubscription { ... }// packages/stripe/v2024_06_20/subscribers/renewal.jadposubscriber renewal {    on: InvoicePaid    run: announce_renewal(event)}// domains/billing/actions/subscribe.jadpovar started: Stripe.StartedSubscription = attempt stripe.start_subscription(request) {    card_declined: PaymentDeclined}

A package action, compiled

Here a package's action calls its own operations, one of which the package keeps to itself, and passes each answer to the next. The app calls the action through its connection and maps its failure at the call. A package action's changing calls are steps of the action that calls it (Several changing calls through a package). The choices are in "Package actions and recipes", further down this page.

The card can be declined after the customer was created. So the package's surface lists after_effect DeclinedCard for start_subscription, and the app's call of it gets the warning step.late_failure: a point that can end subscribe for good once something took effect (Background work). Here the empty customer left behind is harmless, so the warning is declined with that reason.

example
/// Creates the customer, then attaches the card to it.public action start_subscription(input: PayRequest) fails DeclinedCard -> PayReply {    var customer: PayReply = customers.create(input)    return attempt cards.attach(        PayRequest {            reference: customer.id        }    )}
example
use app { pay }use billing/failures { PaymentRefused }use shared/providers/acme_pay/// Starts a subscription: two steps, run by a subscriber.decline step.late_failure: "A declined card leaves an empty customer at Acme Pay, which costs nothing"action subscribe(tier: AcmePay.Reference) fails PaymentRefused -> AcmePay.PayReply {    return attempt pay.start_subscription(        AcmePay.PayRequest {            reference: tier        }    ) {        declined_card: PaymentRefused    }}

A package subscriber, compiled

Here a package's subscriber hears an event its webhooks bring, which the package keeps to itself. It runs the package's action, which looks the subscription up through the connection the webhook came in on and emits the package's own public event. The app's subscriber keeps that event. The choices are under "Package subscribers and recipes in sample/", further down this page. The event the action emits gives no wire_name:, because wire_name: marks an event the provider sends.

example
/// An invoice was paid: all Acme Pay's webhook says, kept to the package.type InvoicePaid = Event {    wire_name: "invoice.paid"    occurred_at: Instant    subscription: Reference}
example
/// A subscription was renewed, with its tier: the package's own event.public type SubscriptionRenewed = Event {    subscription: Reference    tier: Reference    paid_at: Instant}
example
/// Looks up the subscription an invoice paid for, and announces it.action announce_renewal(event: InvoicePaid) -> Unit {    var renewed_subscription: Subscription = subscriptions.retrieve(        SubscriptionAsked {            id: event.subscription        }    )    emit SubscriptionRenewed {        subscription: renewed_subscription.id        tier: renewed_subscription.tier        paid_at: event.occurred_at    }}
example
/// Turns a paid invoice into a renewal.subscriber renewal {    on: InvoicePaid    run: announce_renewal(event)}
example
use billing/actions { record_renewal }use shared/providers/acme_pay/// Keeps each renewal Acme Pay announces.subscriber renewed {    on: AcmePay.SubscriptionRenewed    run: record_renewal(event)}

A signed invoice.paid reaches renewal once, and its lookup is sent through payments with that connection's credential. The renewal it emits reaches renewed through the app's outbox.

Polls

Polling turns an API without webhooks into events. polls: in provider.jadpo names an operation, a default interval and the event emitted when the answer changes. The runtime calls the operation on that interval and keeps what it last saw, as runtime state, like webhook de-duplication. It emits an event only when the answer changes. A poll runs only while something subscribes to its event, and the connection can change the interval: polls: { latest: 6h }.

packages/exchange_rates/v1/provider.jadpo
// packages/exchange_rates/v1/provider.jadpoprovider {    protocol: Protocol.json_http    hosts: {        live: "https://api.rates.example"    }    credential: CredentialScheme.bearer    polls: {        latest: {            call: rates.latest            every: 1d            emits: RatesPublished        }    }}

Polls are for feeds that don't depend on your app's data, such as exchange rates or a price list. To check each of your own records, such as each parcel still in transit, write a schedule in your app that calls the package, one event per record. A package can give that as a recipe.

Here a poll of Acme Fuel's price list emits the package's own public PricesPublished, made from its answer. The operation takes nothing and changes nothing. The connection polls it every six hours instead of once a day, and the app keeps each change. fetched_at, which the event doesn't carry, never counts as a change. The choices are under "How polls and max_concurrent: run", further down this page.

example
application {    connections: {        main: {            driver: Driver.postgres        }        fuel: {            package: FuelPrices.v1            domains: [pricing]            credential: config.fuel_key            environment: config.fuel_environment            max_concurrent: 2            polls: {                latest: 6h            }        }    }    events: {        store: main    }}
example
/// Acme Fuel's price list, which sends no webhooks.provider {    protocol: Protocol.json_http    hosts: {        live: "https://api.fuel-prices.example"        sandbox: "https://sandbox.fuel-prices.example"    }    credential: CredentialScheme.bearer    max_concurrent: 4    polls: {        latest: {            call: pump_prices.latest            every: 1d            emits: PricesPublished        }    }}
example
/// The day's pump prices, published.public type PricesPublished = Event {    region: Text    diesel_pence: Int    petrol_pence: Int}
example
use pricing/actions { keep_prices }use shared/providers/fuel_prices/// Keeps each change of price.subscriber prices_published {    on: FuelPrices.PricesPublished    run: keep_prices(event)}

The tick, the runtime's regular check for background work that is due, calls pump_prices.latest through fuel with its credential once the poll is due. It keeps PricesPublished and its deliveries only when the prices changed, and tries a failed call again a minute later. Without the subscriber the poll never runs. Under max_concurrent: 1 a call waits for the connection's slot, and a delivery that would call through it waits in the outbox.

Recipes

A package may ship recipes/: complete wiring examples (schedules, actions, entities) that jadpo package recipe <package> <recipe> --into <domain> copies into a domain, after which they are the app's own code. Each is compiled and tested against the package's sample/ app at publish and listed in the surface. Library packages may ship recipes too.

jadpo package check's recipes row compiles each recipe on its own, copied into the one domain of the package's sample/ app as jadpo package recipe copies it, the package placed as the sample's own integration; a recipe that doesn't compile is shown at its own path, recipes/<recipe>/..., and a package with recipes and no such sample is publish.sample_missing. The choices are under "Package subscribers and recipes in sample/".

Not built yet: services run by Jadpo (runs:), and running a recipe's tests in sample/.

Markdown for agents: /docs/what-package-code-may-do.md.Built from Jadpo main at aee2e8f.