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: Data

Data

Lifecycle

A lifecycle describes the stages a record goes through and when it drops out of normal view. The common case is soft delete: deleting a todo hides it everywhere at once but keeps it for 30 days, so it can be investigated or restored, and then removes it for good.

example
type TodoTitle = Text {    max_length: 200}entity Todo {    id: Uuid    title: TodoTitle    deleted_at: Instant?    lifecycle: {        visible: deleted_at == none  // which records normal reads can see        transitions: {            delete: {  // a named, allowed stage change                when: deleted_at == none  // only allowed when this is true                set: {                    deleted_at: clock.now  // what the change sets                }            }        }        purge: {  // permanent deletion of hidden records            after: 30d  // how long a hidden record is kept            since: deleted_at  // counted from when it was hidden        }    }}

A lifecycle has three parts:

  • visible: says which records normal reads can see. Every read, list and count leaves out records that aren't visible, and reading one by id reports not_found. Nothing else has to remember to filter them.
  • transitions: are named, allowed stage changes, and the only way to change the fields a lifecycle owns. Each has a name, an optional when: that must be true for it to run, and a fixed set: saying what it changes. The generated operation Todo.delete(todo_id) runs the delete transition.
  • purge: deletes hidden records for good once they have been hidden for after:, counted from the since: field. It runs in small, audited batches. Code can't hard-delete a record that has a lifecycle.

visible: and when: are conditions on the record's own fields. They may use comparisons (==, !=, <, <=, >, >=), and, or, none checks and clock.now, so scheduled publishing works. A starting value that Jadpo sets goes in initial:; here every new story starts as a draft:

example
type HeadlineText = Text {    max_length: 120}type ReviewStage = Enum {    draft    in_review    approved}entity Story {    id: Uuid    headline: HeadlineText    stage: ReviewStage    published_at: Instant?    lifecycle: {        visible: published_at != none and published_at <= clock.now        initial: {            stage: ReviewStage.draft  // every new story starts as a draft        }        transitions: {            submit: {                when: stage == ReviewStage.draft                set: {                    stage: ReviewStage.in_review                }            }        }    }}

The conditions can't mention other records or who is asking. That is Policy, and keeping lifecycle conditions this simple lets them become database indexes.

The details:

  • Bare fields. A lifecycle's conditions name the record's fields bare, as above (deleted_at, not Todo.deleted_at); a qualified one is data.lifecycle_condition, with the fix. in and not aren't among the operators; the opposite comparison says the same.
  • What a lifecycle owns. Every field a transition sets, and every field initial: gives, changes only through the transitions. A create, update or patch that gives one is data.lifecycle_owned_field (Reads and writes). A new record gets its initial: value, or none; so a required field a transition sets needs an initial: value (data.lifecycle_initial_required). Values in set: and initial: are fixed: literals, enum values, none and clock.now.
  • Delete. Todo.delete runs the lifecycle's delete transition, which changes the record and keeps it. An entity with a lifecycle but no delete transition can't be deleted by code at all. Neither can a record whose removal would cascade to one with a lifecycle (data.lifecycle_hard_delete). Only purge removes such records.
  • Transitions see visible records only. A transition is an update, so it reaches a record only while visible: holds. A hidden record is the entity's not-found failure, and restoring one needs a decision of its own. A when: that asks only what visible: asks can't refuse a record that is there. Any other when: can, and its callers map that refusal in conflict: (Conflicts).
  • How long purge waits. after: takes a duration written in place (30d) or a Duration setting (config.soft_delete_retention, Configuration and time), whose value is the one the app started with. A record is purged once visible: doesn't hold and its since: field is older than the run's time less after:.
  • How purge deletes. Each batch deletes at most 500 records, oldest first, in its own transaction. References that cascade and those cleared do what they do for any delete. A record that a restricted reference holds stays until nothing holds it, and never stops the rest going. That includes a hold through any chain of cascades, even one that comes back to its own entity (a folder inside a folder), followed as deep as the data goes.
  • Who runs purge. Purge is Jadpo's own operation, outside every action and policy. The runtime's purge entry point runs it, and the built-in hourly purge schedule calls it (The purge schedule). Each run reports how many records each batch deleted, which the schedule logs.

Markdown for agents: /docs/lifecycle.md.Built from Jadpo main at a707efb.