Coding harness memory systems, any number of orchestration systems, and even tools like OpenClaw are all built around a single thing: a collection of files that any agent uses to coordinate its activities. Even skills and basic instructions take the form of the humble Markdown file. These are quick and easy to implement and rely on instructions that are fairly context-efficient — no need to introduce the infrastructural overhead of MCP servers or even a new CLI.
Ragtime’s Score lets you take that simple approach and run with it. By combining FUSE with our Starlark rule engine we can implement complex dynamic behavior through what appears to be a plain filesystem. This means we can now use simple tools like cat, echo, and mv to drive much more complicated APIs.
This makes agent coordination largely harness-agnostic. Any agent that can read and write files can participate.
How It Works#
The Score has two halves that work together:
The FUSE filesystem (rt mount) exposes a live virtual filesystem at ~/.ragtime/fs/. Directories and files in this tree are not stored on disk — they are backed by SQLite records served dynamically. When an agent writes a file, the filesystem intercepts the write and fires a VFS event into the rule engine.
The rule engine listens for those VFS events — vfs-write, vfs-move, vfs-delete — and runs Starlark scripts that read the event data and update the database. The database, in turn, determines what appears in each directory on the next read.
The result is a filesystem that behaves like an API. Writing a file is a create or update. Moving a file is a state transition. Deleting a file removes the record. Listing a directory is a filtered query.
Declaring a Namespace#
Any rule can create directories in the FUSE mount by adding a vfs: block:
vfs:
- root: my-data # creates ~/.ragtime/fs/my-data/
table: my_records # SQLite table backing this namespace
dirs:
- name: new
filter: {status: new}
writable: true # allows echo/mv into this directory
- name: processed
filter: {status: processed}The filter: on each directory controls which records appear there — my-data/new/ only shows rows where status == "new". Ragtime creates the SQLite table automatically on first write. The Go filesystem layer is fully generic; every directory tree you see under ~/.ragtime/fs/ was declared this way by a rule, including the built-in ones.
An Example: The Task System#
To demonstrate how this all works, let’s walk through the built-in task tracking system step by step — both the shell commands an agent would run, and exactly what happens inside The Score at each one.
When you start Ragtime and mount the filesystem, ~/.ragtime/fs/tasks/ appears with six state directories:
% ls ~/.ragtime/fs/tasks/
active blocked done failed inbox pendingThese directories are declared entirely by a single rule — builtin:task-state. Here it is in full, and then we’ll trace what it does at each step:
name: builtin:task-state
priority: -100
vfs:
- root: tasks
table: tasks
dirs:
- name: inbox
filter: {state: inbox}
writable: true
- name: pending
filter: {state: pending}
writable: true
- name: active
filter: {state: active}
writable: true
- name: blocked
filter: {state: blocked}
writable: true
- name: done
filter: {state: done}
writable: true
- name: failed
filter: {state: failed}
writable: true
match:
event: "vfs-write|vfs-move|vfs-delete"
vfs_path: "tasks/**"
actions:
- type: starlark
script: |
parts = event.vfs_path.split("/")
if len(parts) < 3:
pass
elif parts[2].startswith("."):
pass # skip macOS metadata files
else:
tasks = db.table("tasks")
if event.event_type == "vfs-write":
state = parts[1]
task_id = parts[2][:-5] if parts[2].endswith(".json") else parts[2]
data = {}
if event.vfs_data:
decoded = json.decode(event.vfs_data)
if type(decoded) == "dict":
data = decoded
data["state"] = state
tasks.upsert(task_id, data)
elif event.event_type == "vfs-move":
new_state = parts[1]
task_id = parts[2][:-5] if parts[2].endswith(".json") else parts[2]
tasks.update(task_id, {"state": new_state})
elif event.event_type == "vfs-delete":
task_id = parts[2][:-5] if parts[2].endswith(".json") else parts[2]
tasks.delete(task_id)The vfs: block is what creates the six directories. The Starlark action below the match: keeps SQLite in sync as things are written, moved, and deleted.
Step 1: Create a task#
Write a JSON file to tasks/inbox/. The filename becomes the task ID — use something meaningful.
echo '{"title": "Implement login page", "priority": "high"}' \
> ~/.ragtime/fs/tasks/inbox/login-page.jsonWhat happens: FUSE intercepts the write and fires a vfs-write event with the path tasks/inbox/login-page.json and the file contents as event.vfs_data. The Starlark script splits the path, reads parts[1] ("inbox") as the state, strips .json from the filename to get the task ID ("login-page"), decodes the JSON, merges "state": "inbox" into it, and calls tasks.upsert("login-page", data).
The task is now in SQLite. Reading it back confirms the stored record:
% cat ~/.ragtime/fs/tasks/inbox/login-page.json
{
"id": "login-page",
"priority": "high",
"state": "inbox",
"title": "Implement login page",
"updated_at": 1712345678000
}The id and updated_at fields are added automatically by the database layer.
Step 2: Move to pending#
mv ~/.ragtime/fs/tasks/inbox/login-page.json \
~/.ragtime/fs/tasks/pending/login-page.jsonWhat happens: FUSE fires a vfs-move event with the new path tasks/pending/login-page.json. The Starlark script reads parts[1] ("pending") and calls tasks.update("login-page", {"state": "pending"}). This is a merge — only the state field changes. The title and priority set in the previous step are preserved.
Note the difference from vfs-write: a move doesn’t have file content, so the rule uses update rather than upsert. This is what lets you safely rename a task through states without accidentally overwriting its data.
% cat ~/.ragtime/fs/tasks/pending/login-page.json
{
"id": "login-page",
"priority": "high",
"state": "pending",
"title": "Implement login page",
"updated_at": 1712345699000
}Step 3: Start work#
mv ~/.ragtime/fs/tasks/pending/login-page.json \
~/.ragtime/fs/tasks/active/login-page.jsonSame mechanics as before — vfs-move fires, state becomes "active". The task disappears from tasks/pending/ and appears in tasks/active/ on the next read.
Step 4: Update task data mid-flight#
While a task is in progress, you can update its content by writing to it:
echo '{"title": "Implement login page", "priority": "high", "notes": "use JWT, not sessions"}' \
> ~/.ragtime/fs/tasks/active/login-page.jsonWhat happens: This is a vfs-write event, not a move. The Starlark script runs the upsert branch, which decodes the new JSON and overwrites the record — but then immediately sets "state": "active" from the path. So even if you forget to include state in your JSON, the directory you wrote to enforces it.
% cat ~/.ragtime/fs/tasks/active/login-page.json
{
"id": "login-page",
"notes": "use JWT, not sessions",
"priority": "high",
"state": "active",
"title": "Implement login page",
"updated_at": 1712345750000
}Step 5: Mark done#
mv ~/.ragtime/fs/tasks/active/login-page.json \
~/.ragtime/fs/tasks/done/login-page.jsonvfs-move fires, state becomes "done". The task moves directories. All data is preserved.
Step 6: Query by state#
Because each directory is a filtered view of the same SQLite table, listing a directory is equivalent to SELECT * FROM tasks WHERE state = ?:
% ls ~/.ragtime/fs/tasks/done/
login-page.json
% ls ~/.ragtime/fs/tasks/active/
(empty)You can have as many tasks in as many states as you like. Adding a task to a new state is just writing to that directory.
Step 7: Delete a task#
rm ~/.ragtime/fs/tasks/done/login-page.jsonWhat happens: FUSE fires a vfs-delete event. The Starlark script strips .json from the filename and calls tasks.delete("login-page"), removing the record from SQLite. The file is gone from all state directories.
State Transition Summary#
┌─────────────────────────────────────┐
▼ │
inbox → pending → active → done (any mv)
↓
blocked → active
↓
failedAny mv between directories triggers a vfs-move event. The rule handles every transition identically — it reads the destination directory name as the new state. There’s no hardcoded state machine; the valid states are simply whatever directories exist in the vfs: block.
Writing Your Own Namespace#
The task system is built-in, but the same pattern works for anything. Here’s a minimal example of a custom namespace that tracks API requests:
name: my-api-tracker
vfs:
- root: api-requests
table: api_requests
dirs:
- name: queued
filter: {status: queued}
writable: true
- name: processing
filter: {status: processing}
writable: true
- name: complete
filter: {status: complete}
match:
event: "vfs-write|vfs-move|vfs-delete"
vfs_path: "api-requests/**"
actions:
- type: starlark
script: |
parts = event.vfs_path.split("/")
if len(parts) < 3 or parts[2].startswith("."):
pass
else:
reqs = db.table("api_requests")
if event.event_type == "vfs-write":
status = parts[1]
req_id = parts[2][:-5] if parts[2].endswith(".json") else parts[2]
data = {}
if event.vfs_data:
decoded = json.decode(event.vfs_data)
if type(decoded) == "dict":
data = decoded
data["status"] = status
reqs.upsert(req_id, data)
elif event.event_type == "vfs-move":
req_id = parts[2][:-5] if parts[2].endswith(".json") else parts[2]
reqs.update(req_id, {"status": parts[1]})
elif event.event_type == "vfs-delete":
req_id = parts[2][:-5] if parts[2].endswith(".json") else parts[2]
reqs.delete(req_id)Save this to ~/.ragtime/rules/api-tracker.yaml (or .ragtime/rules/ in your project). Ragtime hot-reloads rules, so the ~/.ragtime/fs/api-requests/ directory appears immediately without restarting.