Mutate data
Every application change is an operation declared on an entity or trait. The local client exposes targetless operations under db.mutate and targeted operations under entity.mutate.
Create with a targetless operation
Section titled “Create with a targetless operation”const Task = Ramose.Entity( "task", { title: Ramose.string(), done: Ramose.boolean() }, { operations: (Operation) => ({ createTask: Operation({ self: false, input: S.Struct({ title: S.String }), output: S.Struct({ id: Ramose.EntityId }), run(op, { title }) { const task = op.create({ title, done: false }) return { id: task } }, }), }), },)op.create is available because the operation is targetless and entity-owned. It requires every non-defaulted field and applies protected type and fixed trait bindings inside the authoritative transaction.
The client call is database-scoped:
const receipt = db.mutate.createTask({ title: "Write the guide" })await receipt.queuedawait receipt.committedChange a visible target
Section titled “Change a visible target”setDone: Operation({ input: S.Struct({ done: S.Boolean }), output: S.Struct({}), optimistic: ({ tx, self, input }) => tx.set(self, Task.done, input.done), run(op, { done }) { op.self.set(Task.done, done) return {} },})The call is entity-scoped:
task.mutate.setDone({ done: true })The server requires the exact operation grant, a visible compatible task, valid input, and a valid produced change. The optimistic projection only controls the temporary local view.
Write related entities explicitly
Section titled “Write related entities explicitly”Declare every additional entity an operation may change:
addMember: Operation({ writes: [Membership], input: S.Struct({ userId: Ramose.EntityId }), output: S.Struct({}), run(op, { userId }) { op.put(Membership, { project: op.self, user: userId, role: "member" }) return {} },})The write list is a closed capability, not documentation. References are checked against concrete or derived trait types on the committing basis.
Use generated operations selectively
Section titled “Use generated operations selectively”Entities expose generated create, update, and delete operations unless you override a key. They use the same descriptors, grants, validation, and mutation namespaces as hand-written operations.
Generated update is appropriate only when one grant may change every mutable field it exposes. Override it or add a narrower domain operation when fields have different write rules or a change requires invariants, side effects, or related writes.
Delete and ownership
Section titled “Delete and ownership”op.self.delete() retracts the target. Owned children follow declared cascading references. Other incoming required references can reject deletion; optional or many references may be cleared according to their schema.
Deletion changes the current value without erasing history. Use archive operations when the domain needs reversible lifecycle rather than disappearance.
Design optimistic projections for replay
Section titled “Design optimistic projections for replay”Projections must be pure and must not query the local database. Send desired state ({ done: true }), not commands whose meaning depends on a basis (toggle). Do not optimistically claim uniqueness, authorization, payment success, or any conditional server outcome.
An operation without a projection still queues offline. The UI can show its receipt and pending entity state without inventing a result.
Change an operation safely
Section titled “Change an operation safely”Every operation carries an operation-scoped version derived only from its own public contract: owner and name, target mode, declared input and output, the entity types it admits as a target, the entities it declares as writes, and an author-declared revision. Redeploying the Worker, changing an unrelated definition, and editing documentation all leave it alone.
That version cannot see the body’s source, so a behavior change behind an unchanged contract needs an explicit bump:
setDone: Operation({ revision: 2, input: S.Struct({ done: S.Boolean }), output: S.Struct({}), run(op, { done }) { op.self.set(Task.done, done) return {} },})A queued invocation that pins an older version is refused with operation_changed and has no effect; the client re-mints it against the current operation. A queued invocation that merely predates a redeploy still executes, and an exact retry of one that already committed still returns its original receipt.
Handle receipts
Section titled “Handle receipts”Receipts expose queued, committed, and rejected transitions plus the stable public result. Treat queued as durable local acceptance, not server success. Treat committed as authoritative.
If transport fails after submission, reuse the same receipt and invocation identity. Do not create a new invocation for the same intent. Ramose returns the original result for an identical retry and a conflict for changed input.