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.

Key insight

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

org tasks.org
* 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:
ComponentSyntaxExample
LevelLeading asterisks*, **, ***
TODO keywordAfter asterisksTODO, DONE, IN_PROGRESS
Priority[#A] through [#C][#A] = high, [#C] = low
TitleRemaining text before tagsBuild the parser
TagsColon-delimited at end of line:core:rust:
Properties:PROPERTIES: drawer:ASSIGNEE: alice
BodyText below propertiesFree-form paragraph text
ChildrenDeeper 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.

org Properties drawer
: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.

org Literate source block
#+BEGIN_SRC elixir :tangle lib/my_module.ex
defmodule MyModule do
  def hello, do: "world"
end
#+END_SRC
Human-readable by design

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

org schema.org — Task 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

PropertyPurposeExample
:FILE:Backing org file for this entity typetasks.org
:TODO_STATES:Valid TODO keywords, pipe-separatedTODO | 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

TypeRust typeExample value
stringFieldValue::String"alice"
integerFieldValue::Int42
floatFieldValue::Float3.14
booleanFieldValue::Booltrue
timestampFieldValue::String"2026-02-20T10:30:00"
listFieldValue::List["core", "rust"]
uuidFieldValue::String"a1b2c3d4"

Field tags

TagMappingDescription
:headline:Heading title textThe heading's title becomes this field's value
:todo:TODO keywordMaps to the heading's TODO state
:priority:Priority cookieMaps to [#A], [#B], [#C]
:tags:Heading tagsMaps to the colon-delimited tag list
:body:Body textMaps to the text below the properties drawer
:required:ValidationField must be present on create/update
:index:SQLite indexField 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

org schema.org — Multiple entities
* 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:
TODO_STATES convention

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 mappingSource in ASTExample
: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.

rust Roundtrip invariant
// 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.

1

Mutate — Apply the change to an immutable AST, producing a new OrgDocument. Mutations are pure functions: insert_heading, update_heading, or delete_heading.

2

Serialize — Convert the new AST back to org-mode text. The serializer preserves all formatting: indentation, blank lines, drawer syntax, and block delimiters.

3

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.

4

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.

5

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.

Atomic writes

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

bash Create a task
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"
  }'
json Response
{
  "ok": true,
  "id": "task-a1b2c3"
}

Read

bash Get a single entity
# Get one task by ID
curl http://localhost:4000/api/entities/Task/task-001
bash List all and filter
# 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

bash Update a task (partial)
curl -X PUT http://localhost:4000/api/entities/Task/task-001 \
  -H "Content-Type: application/json" \
  -d '{ "_todo": "DONE", "assignee": "carol" }'

Delete

bash Delete a task
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.

FieldMaps to
_titleHeading title text
_todoTODO keyword (e.g. TODO, DONE, IN_PROGRESS)
_priorityPriority marker (A, B, C)
_tagsHeading tags (colon-delimited)
_bodyBody text below the heading
Batch operations

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

OperatorMeaningJSON example
EqEqual{"field": "assignee", "op": "Eq", "value": "alice"}
NeNot equal{"field": "_todo", "op": "Ne", "value": "DONE"}
LtLess than{"field": "effort", "op": "Lt", "value": "5"}
GtGreater than{"field": "priority", "op": "Gt", "value": "B"}
LteLess than or equal{"field": "effort", "op": "Lte", "value": "8"}
GteGreater than or equal{"field": "effort", "op": "Gte", "value": "3"}
ContainsSubstring match{"field": "_title", "op": "Contains", "value": "parser"}
IsNullField is absent{"field": "assignee", "op": "IsNull", "value": ""}
IsNotNullField is present{"field": "due", "op": "IsNotNull", "value": ""}

REST query examples

bash REST queries
# 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

elixir Backend.Query pipe chain
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

1

Client subscribes with an entity type and optional filters (e.g. Task where assignee = alice).

2

When a mutation happens, PubSub fires on the "file:{filename}" topic for the affected org file.

3

The subscription engine re-evaluates filters against the updated SQLite index and computes the new result set.

4

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

typescript Reactive subscription via @zaius/sdk
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()
Real-time everywhere

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.

org auth.org — module structure
* 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

AttributePurposeExample
:tangleOutput 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.

shell tangle a module
# 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.

elixir lib/auth/token.ex — tangled output
# 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.

shell detangle after editing source
# 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
Two-way sync

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

mermaid workflow.mermaid
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

1

Parse — Rust parses the mermaid text into a StateMachine struct containing states, transitions, and events.

2

Codegen — The struct is compiled to an Elixir module with a transitions/0 function returning the full transition table.

3

Runtime — A GenServer loads the entity's current TODO state, compiles the module, and starts the process.

4

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

No separate config

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

mermaid publishing.mermaid
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}.

Casing matters

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

mermaid Guard syntax
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.

mermaid Action syntax
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.

mermaid Guard + action
Approved --> Shipped : ship[has_stock]/create_shipment

Action resolution

Action names are resolved in two ways depending on their format:

FormatResolves toExample
local_nameNamed org source block in the same filenotify_customer
Module.functionExternal Elixir function callShipping.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.

org workflows.org
#+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
Guard and action scope

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

mermaid fulfillment.mermaid
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

mermaid payment.mermaid
stateDiagram-v2
    Processing --> is_paid : check_payment
    is_paid <<choice>>
    is_paid --> Fulfillment : yes
    is_paid --> PaymentFailed : no
Not real states

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.

Live Editor
Things to try

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

mermaid Workflow definition
stateDiagram-v2
    [*] --> Todo
    Todo --> InProgress : start
    InProgress --> Done : complete

Output: Generated gen_statem module

elixir lib/backend/workflows/task_workflow.ex
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
Key details

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

1

Runtime receives event via send_event/4 — dispatches :gen_statem.call to the workflow process with the event atom.

2

Looks up transition in transitions/0 — the state function matches the event and returns {:next_state, new_state, data, actions}.

3

persist/3 resolves state name to a TODO keyword — snake_case atom :in_progress becomes UPPERCASE keyword IN_PROGRESS.

4

FileManager.mutate updates the heading's TODO keyword — finds the heading by :ID: property and calls update_heading on the AST.

5

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.

Casing matters

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

MethodPathDescription
GET/api/entities/:entityList entities with optional filters
GET/api/entities/:entity/:idGet a single entity by ID
POST/api/entities/:entityCreate a new entity
PUT/api/entities/:entity/:idUpdate an entity (partial)
DELETE/api/entities/:entity/:idDelete an entity

Workflow endpoints

MethodPathDescription
POST/api/workflows/:type/:id/startStart a workflow for an entity
POST/api/workflows/:type/:id/eventSend an event to a running workflow
GET/api/workflows/:type/:id/statusGet current workflow state
DELETE/api/workflows/:type/:idStop and remove a workflow
POST/api/workflows/:type/:id/validateValidate an event without executing

Module endpoints

MethodPathDescription
GET/api/modulesList all literate modules
GET/api/modules/:nameGet module metadata and blocks
POST/api/modules/:name/tangleTangle module to source files
POST/api/modules/:name/detangleSync edits back into org module
GET/api/modules/:name/graphGet module dependency graph

System endpoints

MethodPathDescription
GET/api/schemaGet the parsed schema definition
GET/api/searchFull-text search across entities
GET/api/analytics/pulseWorkspace-wide pulse summary

Workflow API examples

bash Start a workflow
curl -X POST http://localhost:4000/api/workflows/Task/task-001/start \
  -H "Content-Type: application/json"
bash Send an event
curl -X POST http://localhost:4000/api/workflows/Task/task-001/event \
  -H "Content-Type: application/json" \
  -d '{ "event": "start" }'
json Response
{
  "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

typescript Initialize the SDK
import { Zaius } from '@zaius/sdk'

const z = Zaius.connect()

CRUD operations

typescript List, create, update, delete
// 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

typescript Subscribe to live query results
// 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

typescript Ask the AI agent
const result = await z.agent('Summarize my open tasks')
console.log(result.reply)

Framework bindings

typescript React hook
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>
}
typescript Svelte store
import { zaius } from '@zaius/sdk/svelte'

const tasks = zaius('Task', { status: 'active' })

// In template: {#each $tasks as task}...{/each}
WebSocket + REST

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

ToolParametersDescription
workflow_starttype, idStart a workflow process for an entity. Spawns the gen_statem and sets initial state.
workflow_eventtype, id, eventSend an event to a running workflow. Triggers a state transition if valid.
workflow_statustype, idGet the current state of a running workflow process.
workflow_validatetype, id, eventCheck 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.

LayerChecksExample error
SchemaEntity exists in schema.org, required fields present, no unknown fieldsunknown entity "Taks" — no definition found in schema.org
SpellingFuzzy match against known entity and field names using Jaro distanceunknown entity "Taks" — did you mean "Task"? (score: 0.92)
ReferencesReferenced IDs exist, link targets resolve to valid headingsreferenced id "contact-xyz" not found in contacts.org
TransitionsTODO keyword change follows a valid transition in the workflow graphinvalid transition: TODO → DONE (no direct edge, must pass through IN_PROGRESS)
ReservedUnderscore-prefixed fields (_todo, _title) use correct heading-level values"_todo" value "DOING" is not a valid keyword — expected one of: TODO, IN_PROGRESS, DONE
TypesField values match declared types — integer, float, boolean, timestamp coercionfield "effort" expects integer, got "high"
Fuzzy matching

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

ConstructSyntaxExample
Header (optional)stateDiagram-v2stateDiagram-v2
Initial state[*] --> StateName[*] --> Draft
Final stateStateName --> [*]Done --> [*]
TransitionSource --> Target : eventDraft --> Review : submit
GuardSource --> Target : event [guard]Review --> Approved : approve [has_approvals]
ActionSource --> Target : event / actionSubmitted --> Shipped : ship / send_notification
Full labelSource --> Target : event [guard] / actionReview --> Merged : merge [ci_passes] / notify_team
Compound startstate StateName {state Review {
Compound end}}
Choice nodestate check <<choice>>state validate <<choice>>
Comment%% text%% Order fulfillment workflow
Notenote right of State : textnote right of Draft : Initial state
Directiondirection LR | TB | RL | BTdirection 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.

ContextConventionExample
Mermaid diagramPascalCaseInProgress
Elixir atomsnake_case:in_progress
TODO keywordSCREAMING_SNAKEIN_PROGRESS
Compound childparent_childreview_automated

Schema field syntax

Fields are list items inside an entity heading in schema.org. The format is - name :: type :tag1: :tag2:.

PatternSyntaxEffect
Basic field- name :: stringMaps 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.

mermaid orders.mermaid
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.

mermaid code-review.mermaid
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.

org workflows.org — order fulfillment module
* 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
Self-documenting

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.

org 1. Define the entity in schema.org
* 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:
mermaid 2. Define the workflow in a mermaid file
stateDiagram-v2
    [*] --> Draft
    Draft --> Submitted : submit
    Submitted --> Approved : approve
    Approved --> Shipped : ship [has_inventory]
    Shipped --> Delivered : deliver / send_confirmation
    Delivered --> [*]
bash 3. Create records via REST API
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"
  }'
bash 4. Trigger a workflow transition
# 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" }'
typescript 5. Subscribe to live updates via SDK
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.