Actions
An action is a typed write into a source dataset. It's how new rows reach your data through
the same checkpointed path the agent edit loop uses: validated before anything touches disk,
snapshotted before the write, reversible after. You declare an action in the manifest; an agent
runs it over MCP.
What an action can write
Only a source dataset. A file-backed dataset (CSV, JSON, JSONL) or a SQLite table takes
inserts. A derived sql dataset never does, because it's a query,
not a file. Pointing an action at a sql dataset is a named validation error.
Declaring one
An action names its target dataset and a mode. The only mode today is insert, which appends
rows:
"actions": {
"log_meal": {
"dataset": "meals",
"mode": "insert"
}
}That alone inserts rows whose columns match the meals data. Most actions add a fields block
to constrain or annotate specific columns.
The row schema
An action carries no hand-written schema. It derives one from the live data: the compiler
infers each column's type from the dataset, then layers your fields overrides on top. The
result is a strict schema (unknown columns are rejected) that an agent reads before it sends a
single row.
A fields.<column> entry narrows one column:
"actions": {
"log_meal": {
"dataset": "meals",
"mode": "insert",
"fields": {
"meal_type": { "enum": ["breakfast", "lunch", "dinner", "snack"] },
"calories": { "type": "number", "min": 0 },
"source": { "default": "manual" }
}
}
}Each override key does one job:
type: constrain the column tostring,number,boolean, ordate, overriding what was inferred.enum: restrict a string column to a fixed set of values.min/max: numeric bounds.default: a value applied when a row omits the column.description: a note that rides along with the schema, so the agent knows what the column means.
A key in fields that doesn't match a real column is an error. You can only override columns
that exist.
What happens on a write
An agent calls run_action(name, rows). Before a single byte lands:
- Every row is validated against the resolved schema. One bad row (wrong type, a value
outside
min/max, an unknown column) rejects the whole call with an error naming the row index and the field, and nothing is written. - The target file is snapshotted to
.openislands/history/, so the insert is reversible withrollback.
Then the rows land: appended to a flat file (a new CSV row, a push onto a JSON array, a line
added to NDJSON) or INSERTed into the SQLite table. A SQLite insert needs the file and table
to exist already; a flat file is created if it's missing.
Inserts are all-or-nothing and path-confined: an action can only write the one source file
its dataset names. There is no general file write.
Running an action
Actions belong to the agent edit loop, not to a CLI command. An agent:
- Calls
list_actionsto get each declared action and its resolved row JSON Schema (the live schema merged withfields). That schema is its grounding for a valid row. - Calls
run_action(name, rows), which validates and inserts as above, returning the countinsertedand acheckpoint_id.
Surfacing an action to humans
An action isn't agent-only. Drop a form.entry island on a
page and point it at an action, and the runtime renders a form — one typed input per field, the
action's types, enums, ranges, and defaults carried straight through — with a submit button that
inserts a row. It runs the very same write path run_action does: validate, snapshot, insert, then
the bound dataset's islands refresh live. The form is the human-facing mirror of the agent call,
authored by reusing the action rather than re-declaring its fields.
See MCP Server for the full tool surface, and Connectors for syncing
a provider's data into source datasets on a schedule, through this same write path.