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: Reads and writes

Reads and writes

The operations every entity gets

On this page

Every entity gets these operations, called through the entity's name, as Todo.get(todo_id). Only actions in the entity's own domain may call the ones that write. Other domains call that domain's public actions instead.

Operation Does
Todo.get(todo_id) reads one visible record, or fails TodoNotFound
Todo.create { set: { ... } } adds a record
Todo.update(todo) { set: { ... } } changes fields of a record loaded earlier in the action
Todo.update(todo) { patch: input } changes only the fields a request supplied
Todo.update(todo_id) { set: { ... } } changes the record with that id in one step (counters, below)
Todo.delete(todo) removes the record, or runs the lifecycle's delete transition
User.disable(user) runs a lifecycle transition, named after it

An update, delete or transition takes either the record the action loaded earlier or its id. By id, it fails with the entity's not-found failure when no record has that id.

An update gives its fields in set:, or the request's changes with patch:, never both. when_supplied: goes with patch: (both are below).

A create and an update give back the record as stored; a delete gives nothing. Each fills the fields the store owns: a new version 7 UUID as the id on create (Ctx::new_id, from the invocation's seed), and the operation's time in each generated field, on_create once and on_change on every update.

Every write joins the action's transaction. In the compiler's graph of the program, a writes edge runs from the action or function to the entity.

In this action, attempt User.get(owner_id) gives the user, or passes on UserNotFound. Project.create sets every field but id and the generated ones:

example
// domains/planning/actions/start_project.jadpoaction start_project(owner_id: User.id, name: Project.name) fails UserNotFound -> Project {    var owner: User = attempt User.get(owner_id)  // the user, or UserNotFound    return Project.create {        set: {            owner_id: owner.id  // every field but `id` and the generated ones            name: name        }    }}

Transitions

A transition (Lifecycle) is an update of the fields its set: gives. Like an update, it runs on the record loaded earlier or by id, and gives back the record as stored; a delete transition gives nothing. Its block takes only conflict:, since what it changes is fixed in the lifecycle.

Like every update and delete, a transition reaches only a record that visible: lets through, so a hidden record is the entity's not-found failure. A when: that asks more than visible: can refuse a record that is there. Map that refusal in conflict:, as you would a rule, keyed by the transition. Here UserDisabled is what the action fails with when disable's when: doesn't hold:

example
// domains/planning/actions/disable_user.jadpoaction disable_user(user_id: User.id) fails UserNotFound, UserDisabled -> User {    return attempt User.disable(user_id) {        conflict: {            User.disable: UserDisabled  // its `when:` doesn't hold        }    }}

The statement is UPDATE "user" SET "stage" = $1, "disabled_at" = $2 WHERE "id" = $3 AND "stage" = $4, with the stages bound as their column holds them, 'disabled' and 'active' (Every boundary through the contract). When it changes no row, a second statement asks whether the user is there and visible, which tells UserDisabled from UserNotFound. jadpo sql disable_user shows both.

The not-found failure

Nothing else is an entity operation: Todo.fetch(todo_id) is semantic.unknown_callee. Todo.title(value) still builds a value of the field's type (Creating values).

TodoNotFound is the domain's failure of that name in failures/. When the domain declares none, the compiler provides one: kind not_found, public, with no fields, placed at the entity in the graph. When a declared one has a field typed <Entity>.id, a read or write that fails fills it with the id it looked for. A write that can't fail needs no attempt (Attempt).

This domain declares no TodoNotFound, so the compiler provides it:

example
// domains/planning/actions/reschedule_todo.jadpoaction reschedule_todo(todo_id: Todo.id, due_at: DueAt) fails TodoNotFound, DueDateInPast -> Todo {    var todo: Todo = attempt Todo.get(todo_id)  // TodoNotFound: provided    if (due_at <= clock.now) {        reject DueDateInPast    }    return Todo.update(todo) {        set: {            due_at: due_at            reminder_sent_at: none        }    }}

Patches from a request

A patch takes an Input whose fields are optional and match the entity's. A field the request left out keeps its value, and one sent as none is cleared (Values that may be missing). when_supplied: sets further fields when a given field was supplied: here a new due_at also clears reminder_sent_at. A patch with no fields changes nothing and succeeds, as a JSON Merge Patch does, so there is no empty: failure.

example
type TodoChanges = Input {    pick: [Todo.title, Todo.due_at]    optional: all}action edit_todo(todo: Todo, amendment: TodoChanges) -> Todo {    return Todo.update(todo) {        patch: amendment        when_supplied: {            due_at: {                reminder_sent_at: none            }        }    }}

The statement's text is fixed: each field of the input is written CASE WHEN <supplied> THEN <value> ELSE <column> END, and an on_change field changes only when a field was supplied.

A value built without one of its optional Input fields remembers that the field was left out. Only a patch reads that, and the value still equals one given none.

A field whose type can't be none (title above) can't be cleared, so none given to it is the same as leaving it out, and the patch keeps the stored value. That isn't an error, since passing an optional value on into an optional field, such as a page's next cursor, is ordinary code.

Markdown for agents: /docs/the-operations-every-entity-gets.md.Built from Jadpo main at a707efb.