Orgami
A local-first, plain-text ORM where org-mode files are the database.
Plain text, git-versioned
Your data is .org files — human-readable, editable in any text editor, version-controlled with git.
Schema-typed entities
Define entity types in schema.org. Headings become typed records with fields, TODO states, and relations.
Reactive subscriptions
Subscribe to queries. When org files change, subscriptions re-evaluate and push updates via PubSub.
Mermaid workflows
Write state machines in mermaid, compile to Elixir gen_statem processes with full persistence.
Literate programming
Embed source code in org files with tangle targets. Prose, code, and data live together.
Rust core, Elixir server
Rust handles parsing and indexing. Elixir handles concurrency, PubSub, and real-time channels.
The org file is the database. An in-memory index provides fast queries. If you restart the server, it rebuilds from org files. If you edit an org file by hand, the index updates automatically.
The Org-Mode Format
Org-mode is a plain-text markup format. Every entity in your database is a heading in an org file. Headings carry structure: a level, an optional TODO keyword, priority, title, tags, a properties drawer, body text, and nested children.
Heading anatomy
* TODO [#A] Build the parser :core:rust:
:PROPERTIES:
:ID: a1b2c3d4
:ASSIGNEE: alice
:EFFORT: 8
:END:
Design and implement the org-mode parser in Rust.
Must handle nested headings, drawers, and blocks.
** DONE Write the lexer :rust:
** TODO Write the AST builder :rust: | Component | Syntax | Example |
|---|---|---|
| Level | Leading asterisks | *, **, *** |
| TODO keyword | After asterisks | TODO, DONE, IN_PROGRESS |
| Priority | [#A] through [#C] | [#A] = high, [#C] = low |
| Title | Remaining text before tags | Build the parser |
| Tags | Colon-delimited at end of line | :core:rust: |
| Properties | :PROPERTIES: drawer | :ASSIGNEE: alice |
| Body | Text below properties | Free-form paragraph text |
| Children | Deeper headings underneath | ** DONE Write the lexer |
Properties drawer
The properties drawer is a key-value store attached to a heading. Keys are uppercase by
convention. The :ID: property is the unique identifier for the heading and is
used for linking and indexing.
:PROPERTIES:
:ID: f8e7d6c5
:CREATED: 2026-02-20
:ASSIGNEE: bob
:EFFORT: 3
:LOGGED_TIME: 1.5
:END: Source code blocks
Org files support literate programming through source blocks. The :tangle header argument specifies an output file path. When tangled, the code is extracted and
written to that path.
#+BEGIN_SRC elixir :tangle lib/my_module.ex
defmodule MyModule do
def hello, do: "world"
end
#+END_SRC Every org file can be opened and edited in any text editor, Emacs, or VS Code. Your data is never locked inside a binary format or proprietary database.
The Schema System
The schema is defined in schema.org — an org file parsed by the same parser it
configures. Entity definitions are headings tagged :entity:. Field definitions
are list items inside the entity heading.
Defining an entity
* Task :entity:
:PROPERTIES:
:FILE: tasks.org
:TODO_STATES: TODO | IN_PROGRESS | BLOCKED | WAITING | DONE | CANCELLED
:END:
- id :: uuid :required:
- title :: string :headline:
- status :: string :todo:
- priority :: string :priority:
- tags :: list :tags:
- description :: string :body:
- assignee :: string :index:
- effort :: integer
- created :: timestamp :index: Entity properties
| Property | Purpose | Example |
|---|---|---|
:FILE: | Backing org file for this entity type | tasks.org |
:TODO_STATES: | Valid TODO keywords, pipe-separated | TODO | IN_PROGRESS | DONE |
Field definitions
Fields are defined as list items with the syntax: - name :: type :tag1: :tag2:
The name is the field identifier used in queries. The type determines coercion. Tags control how the field maps to org-mode structure.
Field types
| Type | Rust type | Example value |
|---|---|---|
string | FieldValue::String | "alice" |
integer | FieldValue::Int | 42 |
float | FieldValue::Float | 3.14 |
boolean | FieldValue::Bool | true |
timestamp | FieldValue::String | "2026-02-20T10:30:00" |
list | FieldValue::List | ["core", "rust"] |
uuid | FieldValue::String | "a1b2c3d4" |
Field tags
| Tag | Mapping | Description |
|---|---|---|
:headline: | Heading title text | The heading's title becomes this field's value |
:todo: | TODO keyword | Maps to the heading's TODO state |
:priority: | Priority cookie | Maps to [#A], [#B], [#C] |
:tags: | Heading tags | Maps to the colon-delimited tag list |
:body: | Body text | Maps to the text below the properties drawer |
:required: | Validation | Field must be present on create/update |
:index: | SQLite index | Field is indexed for fast queries |
Fields without mapping tags default to the properties drawer. The field name
is uppercased to become the property key (e.g., assignee becomes :ASSIGNEE:).
Complete schema example
* Order :entity:
:PROPERTIES:
:FILE: orders.org
:TODO_STATES: DRAFT | SUBMITTED | APPROVED | SHIPPED | DELIVERED | CANCELLED
:END:
- id :: uuid :required:
- title :: string :headline:
- status :: string :todo:
- customer :: string :index:
- total :: float
- placed :: timestamp :index:
* Contact :entity:
:PROPERTIES:
:FILE: contacts.org
:END:
- id :: uuid :required:
- name :: string :headline:
- email :: string :index:
- phone :: string
- company :: string :index: The pipe character | in :TODO_STATES: separates valid
keywords. All keywords before and after the pipe are accepted. The pipe itself is a
convention — the parser treats every whitespace-separated token as a valid keyword. Order
implies workflow progression but is not enforced at the schema level.
The Data Model
Data flows through a cycle: org files are parsed into an AST, the AST is hydrated into typed EntityRecords for application use, then dehydrated back into AST nodes and
serialized to org text for storage.
Hydrate / Dehydrate cycle
Hydration: AST to EntityRecord
Hydration reads each OrgHeading and extracts field values according to the
schema's field mappings. The result is an EntityRecord — a map of field names
to typed FieldValues.
| Field mapping | Source in AST | Example |
|---|---|---|
:headline: | heading.title | "Build the parser" |
:todo: | heading.todo_keyword | "TODO" |
:priority: | heading.priority | "A" |
:tags: | heading.tags | ["core", "rust"] |
:body: | heading.body | "Design and implement..." |
| (default) | heading.properties[KEY] | :ASSIGNEE: alice becomes "alice" |
Type coercion
Property values in org-mode are always strings. During hydration, the schema's field type
drives coercion: integer fields parse the string to an i64, float fields parse to f64, boolean fields accept "true"/"false", and list fields split on commas or
use the org tag format. If coercion fails, the value falls back to FieldValue::Null.
Dehydration: EntityRecord to AST
Dehydration is the reverse: an EntityRecord is turned back into an OrgHeading. Field values are placed into the correct AST position based on
their mapping tag, and non-mapped fields are written as properties in the :PROPERTIES: drawer.
// The core property: serialization roundtrip is lossless
assert_eq!(
parse(serialize(parse(content))),
parse(content)
); The write path
When an entity is created or updated, the system follows a strict sequence to maintain consistency between the org file, the in-memory AST, and the SQLite index.
Mutate — Apply the change to an immutable AST, producing a new OrgDocument. Mutations are pure functions: insert_heading, update_heading, or delete_heading.
Serialize — Convert the new AST back to org-mode text. The serializer preserves all formatting: indentation, blank lines, drawer syntax, and block delimiters.
Atomic write — Write to a temp file in the same directory, fsync the file, then rename over the target. Both file and directory are fsynced. A crash at any point leaves the old file intact.
Reload — Re-parse the file from disk into the AST, update the ETS cache, and rebuild the SQLite index. This ensures the in-memory state matches what is on disk.
Broadcast — Publish a PubSub event on "file:{filename}". All subscriptions watching entities backed by this file re-evaluate their queries and push updates to connected clients.
All file writes use the temp+fsync+rename pattern. The temp file is created in the same directory as the target (same filesystem) to guarantee POSIX atomic rename. If the process crashes mid-write, the original file is untouched — no partial writes, no corruption.
Entity CRUD
Every entity defined in schema.org gets a uniform REST API. Create, read, update, and delete records using standard HTTP methods against /api/entities/:entity. The server parses your JSON, mutates the org-mode AST, writes atomically, rebuilds the index, and broadcasts changes — all in one request.
Create
curl -X POST http://localhost:4000/api/entities/Task \
-H "Content-Type: application/json" \
-d '{
"title": "Fix parser bug",
"priority": "A",
"assignee": "bob",
"_todo": "TODO"
}' {
"ok": true,
"id": "task-a1b2c3"
} Read
# Get one task by ID
curl http://localhost:4000/api/entities/Task/task-001 # List all tasks
curl http://localhost:4000/api/entities/Task
# List with filters
curl "http://localhost:4000/api/entities/Task?_todo=TODO&assignee=alice" Update
curl -X PUT http://localhost:4000/api/entities/Task/task-001 \
-H "Content-Type: application/json" \
-d '{ "_todo": "DONE", "assignee": "carol" }' Delete
curl -X DELETE http://localhost:4000/api/entities/Task/task-001 Reserved field names
Fields prefixed with underscore map to heading-level attributes in the org-mode AST, not to properties in the :PROPERTIES: drawer.
| Field | Maps to |
|---|---|
_title | Heading title text |
_todo | TODO keyword (e.g. TODO, DONE, IN_PROGRESS) |
_priority | Priority marker (A, B, C) |
_tags | Heading tags (colon-delimited) |
_body | Body text below the heading |
For atomic multi-entity operations, use POST /api/batch. All operations in the batch succeed or fail together — no partial writes.
Queries & Filters
The query system supports predicate-based filtering against the org-orm SQLite index. Predicates compare a field to a value using an operator. You can pass filters as query params for simple equality, or as a JSON filters param for full operator support.
Query operators
| Operator | Meaning | JSON example |
|---|---|---|
Eq | Equal | {"field": "assignee", "op": "Eq", "value": "alice"} |
Ne | Not equal | {"field": "_todo", "op": "Ne", "value": "DONE"} |
Lt | Less than | {"field": "effort", "op": "Lt", "value": "5"} |
Gt | Greater than | {"field": "priority", "op": "Gt", "value": "B"} |
Lte | Less than or equal | {"field": "effort", "op": "Lte", "value": "8"} |
Gte | Greater than or equal | {"field": "effort", "op": "Gte", "value": "3"} |
Contains | Substring match | {"field": "_title", "op": "Contains", "value": "parser"} |
IsNull | Field is absent | {"field": "assignee", "op": "IsNull", "value": ""} |
IsNotNull | Field is present | {"field": "due", "op": "IsNotNull", "value": ""} |
REST query examples
# Simple equality filter via query params
curl "http://localhost:4000/api/entities/Task?_todo=TODO"
# Multiple filters
curl "http://localhost:4000/api/entities/Task?_todo=TODO&assignee=alice"
# Full operator support via JSON filters param
curl "http://localhost:4000/api/entities/Task?filters=[\
{"field":"_todo","op":"Ne","value":"DONE"},\
{"field":"effort","op":"Lt","value":"5"}\
]"
# Ordering and pagination
curl "http://localhost:4000/api/entities/Task?_todo=TODO&order=priority&limit=10" Elixir query builder
Backend.Query.new("Task")
|> Backend.Query.where("_todo", :ne, "DONE")
|> Backend.Query.where("assignee", :eq, "alice")
|> Backend.Query.where("effort", :lt, "5")
|> Backend.Query.order("priority")
|> Backend.Query.limit(10)
|> Backend.Query.run() The query builder translates Elixir atoms (:eq, :ne, :lt) into the Rust enum strings (Eq, Ne, Lt) before crossing the NIF boundary. All queries hit the SQLite index — they never scan org files directly.
Subscriptions
Subscriptions are reactive queries. Instead of polling for changes, you declare what data you care about and the system pushes updates whenever the underlying org files change. This works across Phoenix Channels, the TypeScript SDK, and Flutter FFI.
How it works
Client subscribes with an entity type and optional filters (e.g. Task where assignee = alice).
When a mutation happens, PubSub fires on the "file:{filename}" topic for the affected org file.
The subscription engine re-evaluates filters against the updated SQLite index and computes the new result set.
New results are pushed to the client via Phoenix Channel — no additional request needed.
File-scoped evaluation
Subscriptions are scoped to files, not individual entities. Each entity type maps to a backing org file via EntityDef.file in the schema. When a file changes, only subscriptions for entity types backed by that file are re-evaluated. A change to tasks.org triggers Task subscriptions but not Contact subscriptions backed by contacts.org. This keeps the evaluation cost proportional to the number of affected subscriptions, not the total number of active subscriptions.
SDK example
import { Zaius } from '@zaius/sdk'
const z = Zaius.connect()
// Subscribe to alice's tasks — callback fires on every change
const unsub = z.subscribe(
'Task',
{ assignee: 'alice' },
(tasks) => {
console.log('Updated tasks:', tasks)
}
)
// Later: unsubscribe to stop receiving updates
unsub() Subscriptions work across all clients: Flutter desktop via FFI, SDK web apps via Phoenix Channels, and agent sessions via the agent channel. The same PubSub broadcast reaches all of them.
Literate Programming
Org modules mix prose and code in a single file. Documentation, implementation, and data live together — the module is the specification. Code blocks declare their tangle targets, and the system extracts them into real source files on demand.
* Auth Module
:PROPERTIES:
:ID: auth-module
:END:
This module handles user authentication. Tokens are JWTs signed
with a workspace-scoped secret and verified on every request.
** Token generation
:PROPERTIES:
:ID: auth-tokens
:END:
Tokens expire after 24 hours. The payload includes the user ID
and a list of granted scopes.
#+NAME: token-helpers
#+BEGIN_SRC elixir :tangle lib/auth/token.ex
defmodule Auth.Token do
def generate(user_id, scopes) do
payload = %{sub: user_id, scopes: scopes, exp: expiry()}
Phoenix.Token.sign(endpoint(), "user", payload)
end
defp expiry, do: System.system_time(:second) + 86_400
end
#+END_SRC
** Token verification
:PROPERTIES:
:ID: auth-verify
:DEPENDS_ON: auth-tokens
:END:
Verification checks expiry and signature. Returns the payload
or an error tuple.
#+BEGIN_SRC elixir :tangle lib/auth/token.ex
def verify(token) do
case Phoenix.Token.verify(endpoint(), "user", token) do
{:ok, payload} -> {:ok, payload}
{:error, reason} -> {:error, reason}
end
end
#+END_SRC Block attributes
| Attribute | Purpose | Example |
|---|---|---|
:tangle | Output file path where the code block is extracted to | :tangle lib/auth/token.ex |
#+NAME: | Names a block so other blocks can reference it | #+NAME: token-helpers |
<<name>> | Includes the contents of another named block inline | <<token-helpers>> |
Dependencies
The :DEPENDS_ON: property declares that one section requires another.
When tangling, blocks are ordered so that dependencies are emitted first. In the
example above, "Token verification" depends on "Token generation" — the generate function will always appear before verify in the tangled output file, regardless
of their order in the org document.
Tangle & Detangle
Tangle
Tangling extracts code blocks from an org module and writes them to their
target files. Each block's :tangle header argument specifies
the output path. Blocks targeting the same file are concatenated in
dependency order.
# via REST API
curl -X POST http://localhost:4000/api/modules/auth/tangle
# via mix task
mix zaius tangle auth The tangled output includes traceability comments that link each section of generated code back to its source heading in the org file. These comments are machine-readable and power the detangle process.
# zaius-module: auth
# zaius:auth-tokens:0
defmodule Auth.Token do
def generate(user_id, scopes) do
payload = %{sub: user_id, scopes: scopes, exp: expiry()}
Phoenix.Token.sign(endpoint(), "user", payload)
end
defp expiry, do: System.system_time(:second) + 86_400
end
# zaius:auth-verify:0
def verify(token) do
case Phoenix.Token.verify(endpoint(), "user", token) do
{:ok, payload} -> {:ok, payload}
{:error, reason} -> {:error, reason}
end
end Detangle
Detangling is the reverse: when you edit a tangled source file directly,
detangle syncs those changes back into the org module. It uses the
traceability comments to map each code section to its origin heading,
then updates the corresponding #+BEGIN_SRC block in place.
# sync edits from lib/auth/token.ex back to auth.org
curl -X POST http://localhost:4000/api/modules/auth/detangle
# via mix task
mix zaius detangle auth Edit org → tangle → code updates. Edit code → detangle → org updates. The traceability comments make both directions lossless. You can work in whichever environment suits you — the org module and the source files stay in sync.
Mermaid Basics
Orgami uses standard mermaid stateDiagram-v2 syntax to define state machines.
The stateDiagram-v2 header is optional. Every diagram needs an initial [*] transition to declare the starting state.
A minimal state machine
stateDiagram-v2
[*] --> Todo
Todo --> InProgress : start
InProgress --> Done : complete
Done --> [*] This produces 3 states (Todo, InProgress, Done) and 2 events (start, complete). The [*] pseudo-state marks the entry and exit points
of the machine.
How it fits together
Parse — Rust parses the mermaid text into a StateMachine struct containing states, transitions, and events.
Codegen — The struct is compiled to an Elixir module with a transitions/0 function returning the full transition table.
Runtime — A GenServer loads the entity's current TODO state, compiles the module, and starts the process.
Persist — Each transition writes back to the org file as a TODO keyword change, which triggers the standard write pipeline (atomic write → index rebuild → PubSub broadcast).
The pipeline
The mermaid diagram is the workflow definition. There is no YAML, no JSON config, no admin UI. Edit the diagram, and the state machine updates on next load.
Transitions & Events
Every arrow in the diagram is a transition. The syntax is Source --> Target : event_name. The event name becomes the function you call
to trigger the transition at runtime.
Transition syntax
stateDiagram-v2
[*] --> Draft
Draft --> Review : submit
Review --> Draft : request_changes
Review --> Published : approve
Published --> [*] This gives you four transitions: submit, request_changes, approve, and the implicit terminal transition from Published.
Each event is only valid from its source state — calling submit on a Published entity returns {:error, :invalid_transition}.
State names follow different casing conventions as they cross boundaries: PascalCase in mermaid diagrams → snake_case atoms in Elixir code → SCREAMING_SNAKE_CASE TODO keywords in org files.
For example, InProgress becomes :in_progress in Elixir
and IN_PROGRESS in the org heading.
Guards & Actions
Guards gate transitions on boolean conditions. Actions fire side effects after a transition succeeds. Both are declared inline on the transition arrow.
Guards
A guard is a boolean check that must return true for the transition to
proceed. Attach a guard with square brackets after the event name: A --> B : event[guard_name].
Processing --> Shipped : ship[has_stock] If the guard returns false, the transition is rejected and the caller
receives {:error, :guard_failed}. The entity stays in its current state.
Actions
An action is a side effect that runs after a transition completes. Attach an action with
a slash after the event name: A --> B : event/action_name.
Pending --> Approved : approve/notify_customer Combined guard + action
Guards and actions compose on the same transition: A --> B : event[guard]/action. The guard runs first. If it passes, the
transition fires and then the action executes.
Approved --> Shipped : ship[has_stock]/create_shipment Action resolution
Action names are resolved in two ways depending on their format:
| Format | Resolves to | Example |
|---|---|---|
local_name | Named org source block in the same file | notify_customer |
Module.function | External Elixir function call | Shipping.create_shipment |
Local action as org block
A local action name resolves to a #+NAME: source block in the same org
file. The block receives the entity record and transition context as arguments.
#+NAME: notify_customer
#+BEGIN_SRC elixir
fn entity, _ctx ->
customer_id = entity["customer"]
Notifications.send(customer_id, "Your order has been approved.")
end
#+END_SRC Guards and actions both receive the full entity record and a context map containing the event name, source state, and target state. Guards must return a boolean. Actions can return anything — their return value is discarded.
Compound States
Compound states group related sub-states under a parent using state Name { ... }. This keeps large diagrams organized by nesting
related stages together.
Nested state syntax
stateDiagram-v2
state Fulfillment {
[*] --> Picking
Picking --> Packing : picked
Packing --> Shipping : packed
} Compound states are flattened during codegen. A sub-state inside a parent
is prefixed with the parent name, separated by an underscore. For example, Picking inside Fulfillment becomes the :fulfillment_picking atom in Elixir and the FULFILLMENT_PICKING TODO keyword in org files.
This means you can nest for visual clarity in the diagram without any runtime overhead — the GenServer sees a flat list of states with fully-qualified names.
Choice Nodes
Choice nodes let you branch based on a condition without duplicating events across multiple transitions. Instead of writing two separate transitions from the same source, you route through a choice node that resolves the branch.
Choice node syntax
stateDiagram-v2
Processing --> is_paid : check_payment
is_paid <<choice>>
is_paid --> Fulfillment : yes
is_paid --> PaymentFailed : no Choice nodes are not real states in the generated state machine. They are resolved
at codegen time into guard logic on the incoming transition. The is_paid node above compiles to a single check_payment event on Processing with two guarded branches — one leading to Fulfillment and one to PaymentFailed.
Mermaid Playground
Edit the mermaid stateDiagram below and hit Render to see it update live. This is the same diagram syntax used to define workflows in orgami.
Add a guard condition: Active --> Paused : pause [isRunning]
Add an action: Idle --> Active : start / initialize()
Add a compound state: nest states inside state Active { ... }
Generated Elixir Code
When you define a workflow as a mermaid stateDiagram, the compiler generates a full :gen_statem module. Here is the input diagram and its generated output.
Input: Mermaid diagram
stateDiagram-v2
[*] --> Todo
Todo --> InProgress : start
InProgress --> Done : complete Output: Generated gen_statem module
defmodule Backend.Workflows.TaskWorkflow do
@moduledoc """
Generated gen_statem workflow for Task entity.
Source: mermaid stateDiagram in schema.org
"""
@behaviour :gen_statem
# --- Public API ---
def start_link(args), do: :gen_statem.start_link(__MODULE__, args, [])
# --- Callbacks ---
@impl true
def callback_mode, do: [:state_functions, :state_enter]
@impl true
def init(%{entity_id: id, entity_type: type} = args) do
data = %{
entity_id: id,
entity_type: type,
started_at: DateTime.utc_now()
}
{:ok, :todo, data}
end
# --- Transition table ---
def transitions do
[
%{from: :todo, event: :start, to: :in_progress, guard: nil},
%{from: :in_progress, event: :complete, to: :done, guard: nil}
]
end
# --- State functions ---
def todo(:enter, _old_state, data), do: {:keep_state, data}
def todo({:call, from}, :start, data) do
{:next_state, :in_progress, data, [{:reply, from, :ok}]}
end
def in_progress(:enter, _old_state, data), do: {:keep_state, data}
def in_progress({:call, from}, :complete, data) do
{:next_state, :done, data, [{:reply, from, :ok}]}
end
def done(:enter, _old_state, data), do: {:keep_state, data}
# --- Catch-all ---
@impl true
def handle_event({:call, from}, event, state, _data) do
{:keep_state_and_data, [{:reply, from, {:error, {:invalid_event, event, state}}}]}
end
end All events use {:call, from} semantics for synchronous request-reply. The transitions/0 function returns a lookup table that the Runtime uses to validate events before dispatching. The catch-all handle_event/4 returns {:error, {:invalid_event, event, state}} for any unrecognized event in any state.
State Persistence
Workflow state transitions are persisted back to the org file. When a gen_statem process transitions to a new state, the heading's TODO keyword is updated atomically. The full pipeline ensures the in-memory state, on-disk file, ETS cache, SQLite index, and all subscribers stay in sync.
Persistence pipeline
Runtime receives event via send_event/4 — dispatches :gen_statem.call to the workflow process with the event atom.
Looks up transition in transitions/0 — the state function matches the event and returns {:next_state, new_state, data, actions}.
persist/3 resolves state name to a TODO keyword — snake_case atom :in_progress becomes UPPERCASE keyword IN_PROGRESS.
FileManager.mutate updates the heading's TODO keyword — finds the heading by :ID: property and calls update_heading on the AST.
Atomic write → ETS reload → Index rebuild → PubSub broadcast — the file is written via temp+fsync+rename, ETS is updated, the SQLite index is rebuilt, and "file:{filename}" is broadcast so all subscribers re-evaluate.
The casing chain is: PascalCase in mermaid (InProgress) → snake_case as Elixir atom (:in_progress) → UPPERCASE as org-mode TODO keyword (IN_PROGRESS). Each conversion is deterministic. The schema's :TODO_STATES: property must list the UPPERCASE form.
REST API
The Phoenix server exposes a uniform REST API for entities, workflows, modules, and system information. All endpoints return JSON.
Entity endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/entities/:entity | List entities with optional filters |
| GET | /api/entities/:entity/:id | Get a single entity by ID |
| POST | /api/entities/:entity | Create a new entity |
| PUT | /api/entities/:entity/:id | Update an entity (partial) |
| DELETE | /api/entities/:entity/:id | Delete an entity |
Workflow endpoints
| Method | Path | Description |
|---|---|---|
| POST | /api/workflows/:type/:id/start | Start a workflow for an entity |
| POST | /api/workflows/:type/:id/event | Send an event to a running workflow |
| GET | /api/workflows/:type/:id/status | Get current workflow state |
| DELETE | /api/workflows/:type/:id | Stop and remove a workflow |
| POST | /api/workflows/:type/:id/validate | Validate an event without executing |
Module endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/modules | List all literate modules |
| GET | /api/modules/:name | Get module metadata and blocks |
| POST | /api/modules/:name/tangle | Tangle module to source files |
| POST | /api/modules/:name/detangle | Sync edits back into org module |
| GET | /api/modules/:name/graph | Get module dependency graph |
System endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/schema | Get the parsed schema definition |
| GET | /api/search | Full-text search across entities |
| GET | /api/analytics/pulse | Workspace-wide pulse summary |
Workflow API examples
curl -X POST http://localhost:4000/api/workflows/Task/task-001/start \
-H "Content-Type: application/json" curl -X POST http://localhost:4000/api/workflows/Task/task-001/event \
-H "Content-Type: application/json" \
-d '{ "event": "start" }' {
"ok": true,
"from": "todo",
"to": "in_progress",
"event": "start"
} TypeScript SDK
The @zaius/sdk package provides a zero-dependency TypeScript client for connecting user-built apps to the orgami backend. It handles both reactive subscriptions over WebSocket and one-shot CRUD over REST.
Connect
import { Zaius } from '@zaius/sdk'
const z = Zaius.connect() CRUD operations
// List with filters
const tasks = await z.list('Task', { status: 'active' })
// Create
const id = await z.create('Task', { title: 'Ship feature', assignee: 'alice' })
// Update (partial)
await z.update('Task', id, { status: 'done' })
// Delete
await z.delete('Task', id) Reactive subscriptions
// Subscribe — callback fires on every change
const unsub = z.subscribe('Task', { status: 'active' }, (tasks) => {
console.log('Active tasks:', tasks.length)
})
// Later: unsubscribe to stop receiving updates
unsub() Agent invocation
const result = await z.agent('Summarize my open tasks')
console.log(result.reply) Framework bindings
import { useZaius } from '@zaius/sdk/react'
function TaskList() {
const { data, loading } = useZaius('Task', { status: 'active' })
if (loading) return <p>Loading...</p>
return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>
} import { zaius } from '@zaius/sdk/svelte'
const tasks = zaius('Task', { status: 'active' })
// In template: {#each $tasks as task}...{/each} Subscriptions use WebSocket connections via Phoenix Channels, joining the entity:{name} topic. CRUD operations use standard REST calls to /api/entities/:entity. The SDK manages both transports transparently.
Agent Tools
The AI agent has access to workflow tools alongside the standard CRUD tools. Workflow tools let the agent start processes, send events, check state, and validate transitions programmatically.
Workflow tools
| Tool | Parameters | Description |
|---|---|---|
workflow_start | type, id | Start a workflow process for an entity. Spawns the gen_statem and sets initial state. |
workflow_event | type, id, event | Send an event to a running workflow. Triggers a state transition if valid. |
workflow_status | type, id | Get the current state of a running workflow process. |
workflow_validate | type, id, event | Check whether an event would be accepted without executing it. Returns valid/invalid with reason. |
The agent also has access to full entity CRUD tools (query, create, update, delete) and memory tools (search, memory_write). Workflow tools compose naturally with CRUD — the agent can query for entities in a given state, send events to transition them, and verify the results, all within a single conversation turn.
Validation Pipeline
Every mutation passes through a six-layer validation pipeline before touching the org file. Each layer checks a different class of error, from schema conformance to type coercion. Validation failures are returned as structured error tuples with actionable messages.
| Layer | Checks | Example error |
|---|---|---|
| Schema | Entity exists in schema.org, required fields present, no unknown fields | unknown entity "Taks" — no definition found in schema.org |
| Spelling | Fuzzy match against known entity and field names using Jaro distance | unknown entity "Taks" — did you mean "Task"? (score: 0.92) |
| References | Referenced IDs exist, link targets resolve to valid headings | referenced id "contact-xyz" not found in contacts.org |
| Transitions | TODO keyword change follows a valid transition in the workflow graph | invalid transition: TODO → DONE (no direct edge, must pass through IN_PROGRESS) |
| Reserved | Underscore-prefixed fields (_todo, _title) use correct heading-level values | "_todo" value "DOING" is not a valid keyword — expected one of: TODO, IN_PROGRESS, DONE |
| Types | Field values match declared types — integer, float, boolean, timestamp coercion | field "effort" expects integer, got "high" |
The spelling layer uses Jaro distance scoring to suggest corrections for typos. An entity or field name with a Jaro distance score above 0.7 is included as a suggestion in the error message. This catches common mistakes like Taks instead of Task, or asignee instead of assignee.
Syntax Reference
Quick-reference tables for mermaid workflow syntax, state name conventions, and schema field definitions. Keep this section open while writing your first workflow.
Mermaid workflow syntax
| Construct | Syntax | Example |
|---|---|---|
| Header (optional) | stateDiagram-v2 | stateDiagram-v2 |
| Initial state | [*] --> StateName | [*] --> Draft |
| Final state | StateName --> [*] | Done --> [*] |
| Transition | Source --> Target : event | Draft --> Review : submit |
| Guard | Source --> Target : event [guard] | Review --> Approved : approve [has_approvals] |
| Action | Source --> Target : event / action | Submitted --> Shipped : ship / send_notification |
| Full label | Source --> Target : event [guard] / action | Review --> Merged : merge [ci_passes] / notify_team |
| Compound start | state StateName { | state Review { |
| Compound end | } | } |
| Choice node | state check <<choice>> | state validate <<choice>> |
| Comment | %% text | %% Order fulfillment workflow |
| Note | note right of State : text | note right of Draft : Initial state |
| Direction | direction LR | TB | RL | BT | direction TB |
State name conventions
State names are transformed as they cross system boundaries. The mermaid diagram is the source of truth — all other forms are derived automatically.
| Context | Convention | Example |
|---|---|---|
| Mermaid diagram | PascalCase | InProgress |
| Elixir atom | snake_case | :in_progress |
| TODO keyword | SCREAMING_SNAKE | IN_PROGRESS |
| Compound child | parent_child | review_automated |
Schema field syntax
Fields are list items inside an entity heading in schema.org. The format is - name :: type :tag1: :tag2:.
| Pattern | Syntax | Effect |
|---|---|---|
| Basic field | - name :: string | Maps to :NAME: in properties drawer |
| Required | - id :: uuid :required: | Validation enforced on create/update |
| Indexed | - email :: string :index: | SQLite index for fast queries |
| Headline mapping | - title :: string :headline: | Maps to heading title text |
| TODO mapping | - status :: string :todo: | Maps to heading TODO keyword |
| Body mapping | - description :: string :body: | Maps to text below the properties drawer |
| Priority mapping | - priority :: string :priority: | Maps to [#A], [#B], [#C] |
| Tags mapping | - tags :: list :tags: | Maps to colon-delimited heading tags |
| Multiple tags | - created :: timestamp :required: :index: | Required and indexed |
Full Examples
Complete workflows from diagram to running code. Each example shows the mermaid definition, the rendered state machine, and how it integrates with the org-mode entity system.
Order fulfillment
A linear workflow with a guard on the ship transition and an action on deliver.
stateDiagram-v2
[*] --> Draft
Draft --> Submitted : submit
Submitted --> Approved : approve
Approved --> Shipped : ship [has_inventory]
Shipped --> Delivered : deliver / send_confirmation
Delivered --> [*]
Submitted --> Cancelled : cancel
Approved --> Cancelled : cancel
Cancelled --> [*] Code review with compound states
The Review state contains two sub-states: Automated (CI checks) and Manual (human review). The compound state is entered via open_pr and exited via approve or request_changes.
stateDiagram-v2
[*] --> Draft
Draft --> Review : open_pr
state Review {
[*] --> Automated
Automated --> Manual : ci_pass
Automated --> Failed : ci_fail
Failed --> Automated : retry
}
Review --> Merged : approve [all_checks_pass] / notify_team
Review --> Draft : request_changes
Merged --> [*] Workflow in an org file
In practice, workflows live inside org headings using #+BEGIN_SRC mermaid :tangle blocks. The heading contains the state diagram, guard and action implementations, and prose
documentation — all in one place.
* Order Fulfillment :workflow:
:PROPERTIES:
:ID: order-fulfillment
:ENTITY: Order
:END:
The order workflow tracks an order from draft through delivery.
Guards prevent shipping without inventory. Actions send
confirmation emails on delivery.
** State Diagram
#+BEGIN_SRC mermaid :tangle workflows/order.mermaid
stateDiagram-v2
[*] --> Draft
Draft --> Submitted : submit
Submitted --> Approved : approve
Approved --> Shipped : ship [has_inventory]
Shipped --> Delivered : deliver / send_confirmation
Delivered --> [*]
#+END_SRC
** Guards
#+BEGIN_SRC elixir :tangle lib/workflows/order_guards.ex
defmodule Workflows.OrderGuards do
def has_inventory(order) do
order["quantity"] <= Inventory.available(order["sku"])
end
end
#+END_SRC
** Actions
#+BEGIN_SRC elixir :tangle lib/workflows/order_actions.ex
defmodule Workflows.OrderActions do
def send_confirmation(order) do
Mailer.deliver(
to: order["customer_email"],
subject: "Order #{order["id"]} delivered",
template: :delivery_confirmation
)
end
end
#+END_SRC The org heading contains the state diagram, guard implementations, action implementations, and prose documentation — all in one place. Tangle extracts the code to source files. Detangle syncs edits back. The heading is the single source of truth for the entire workflow.
Complete entity lifecycle
From schema definition to live subscription in five steps. This shows how all the pieces fit together end-to-end.
* Order :entity:
:PROPERTIES:
:FILE: orders.org
:TODO_STATES: DRAFT | SUBMITTED | APPROVED | SHIPPED | DELIVERED | CANCELLED
:END:
- id :: uuid :required:
- title :: string :headline:
- status :: string :todo:
- customer :: string :index:
- total :: float
- placed :: timestamp :index: stateDiagram-v2
[*] --> Draft
Draft --> Submitted : submit
Submitted --> Approved : approve
Approved --> Shipped : ship [has_inventory]
Shipped --> Delivered : deliver / send_confirmation
Delivered --> [*] curl -X POST http://localhost:4000/api/entities/Order \
-H "Content-Type: application/json" \
-d '{
"title": "Widget order #1042",
"customer": "acme-corp",
"total": 249.99,
"_todo": "DRAFT"
}' # Transition the order from DRAFT to SUBMITTED
curl -X POST http://localhost:4000/api/entities/Order/order-1042/transition \
-H "Content-Type: application/json" \
-d '{ "event": "submit" }' import { Zaius } from '@zaius/sdk'
const z = Zaius.connect()
// Subscribe to all non-delivered orders for acme-corp
const unsub = z.subscribe(
'Order',
{ customer: 'acme-corp', status: { op: 'Ne', value: 'DELIVERED' } },
(orders) => {
console.log('Active orders:', orders.length)
orders.forEach(o => console.log(` ${o.title} — ${o.status}`))
}
)
// The callback fires immediately with current data,
// then again whenever an Order in orders.org changes.