Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: External services

External services

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 in one action. 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.

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:

example
// 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 in one action). 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 (Jobs and events). 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 }.

example
// 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.

Services

A package's own API may be a container Jadpo runs, declared with runs:. The package's actions call it. A service can't call another package or connection.

example
// packages/gotenberg/v8/provider.jadpo/// Gotenberg's HTTP API, run by Jadpo as a container beside the app.provider {    protocol: Protocol.multipart_http    runs: {        image: "gotenberg/gotenberg:8@sha256:…"        port: 3000        asks: ["fonts.googleapis.com"]    }}
  • Only stateless services. A service holds no data and has no volume, and each app instance runs its own copy, so many servers need nothing shared. Anything that holds data (PostgreSQL, Redis) is infrastructure the operator provides and the app reaches through a driver connection.
  • A service never starts until approved. Installing shows the image, its digest and the hosts it asks for; the app names it on a connection, and a new or changed image needs a person's acceptance in the approval report before it runs, so an upgrade cannot swap in a different image unseen. The image's digest is written in the package source, which the package's own digest pins; an own integration in shared/providers/ has no lock entry, and its image is listed the same way.
  • Outbound network needs the app's permission. A package only asks (asks:); the service gets none of those hosts until the connection grants them (outbound: [...]), and a new grant appears in the approval report. Until then jadpo check names the hosts asked for and the line that grants them.
  • Three settings Jadpo always applies when it creates a container: no outbound network unless granted, never privileged, and no access to the host's disk. Jadpo creates the containers itself, so there is no configuration to audit.
  • Reaching a service. A package says only the port its service listens on inside its container, so two packages can both use 3000. Jadpo gives each app a private network that only it and its services are on, and hands each connection the address: the service's name on that network when the app runs in a container, or a loopback port the engine picked when it runs as a plain process. Nobody writes an address.
  • Jadpo is a client, never a container runtime. It talks to whatever engine the machine has through the Docker-compatible API, which Docker and Podman both serve, and leaves pulling, isolation and restart policies to it. An app with no service never needs an engine; when one is needed and none is found, jadpo dev and the deployable say so and how to install one.
  • Docker stays behind the scenes. jadpo services lists each service (package, image, state, health, CPU and memory, restarts, uptime); jadpo services start, stop and restart <name>, logs <name> (--follow, --since) and shell <name> (development only) manage them, each with --json. In production the commands talk to the running app, which supervises its services. A service's output joins the app's OpenTelemetry logs under its name, and a call to it appears in the request's trace as a provider call.
  • Where services run. jadpo dev starts the declared services, and tests keep using fakes. jadpo build writes the configuration that runs each one beside the app, Compose first. The native deployable supervises its services itself. On Workers, which runs no containers, a service is hosted elsewhere and the connection gives host:.

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 a707efb.