HOW IT WORKS

Your AI writes.
Caedos runs.
You decide.

Caedos splits standing work into the part a model is good at (writing the synth) and the part it isn't (running it faithfully and cheaply, tick after tick).

How a synth goes to work

In each step: a slice of the HN keyword watcher. Illustrative data.

  1. 01 · Authoring

    Your AI, in Claude Code or any MCP client, writes the synth, shadows it, deploys it, reads traces and patches from evidence. Then it leaves.

    caedos_validate
    ok
    caedos_shadow
    would_act: []
    caedos_deploy
    leash: required
    caedos_run
    idle: baseline
    The authoring loop →
  2. 02 · Running on Caedos

    Once deployed, the synth runs as a process. Caedos fires it on its trigger, walks six phases per tick, records everything and repairs what it can.

    • 13:30:05 acted queued $0
    • 13:00:03 idle live $0
    • 12:30:04 idle live $0
    • 12:00:02 idle live $0
    Inside a tick →
  3. 03 · Approval

    You, the operator, allow side effects, approve or reject queued actions, read journals and set policy.

    Allow required
    notify_webhook

    alert: New HN stories for your keyword · new_titles: ["Show HN: …"]

    The leash →

Allow is a button in your control room. It isn't one of your AI's tools.

ANATOMY OF A SYNTH

A small program with a goal.

hn-keyword-watcher · annotated
synth:                                   # who it is and when it runs
  name: HN watcher
  goal: Tell me when new Hacker News stories mention my keyword.
  schedule_seconds: 1800
requires_credentials: [WEBHOOK_URL]      # declared, stored in the vault
sources:                                 # OBSERVE: where to look
  - id: hn
    primitive: rest_api
    config:
      url: "https://hn.algolia.com/api/v1/search_by_date?query=claude&tags=story"
      items_path: hits
      max_pages: 1
transforms:                              # OBSERVE: deterministic shaping
  - id: titles
    primitive: extract
    config: { items: $sources.hn.items, field: title }
fast_path:                               # FILTER: what counts as a change
  primitive: fast_path_diff
  inputs:
    current: $transforms.titles.values
    previous: $state.seen_titles
actions:                                 # ACT: what to do about it
  - if: $fast_path.triggered
    primitive: notify_webhook            # side-effecting → leashed
    config:
      url: $secrets.WEBHOOK_URL
      body: { alert: New HN stories for your keyword, new_titles: $fast_path.diff }
  - always: true
    primitive: write_state               # SAVE: memory for next tick
    config: { seen_titles: $transforms.titles.values, checked_at: $tick.started_at }

This one never calls a model. When you want judgement, add a reasoning block. It only runs when the fast path fires.

THE TICK

Most ticks are silent.

Beside each phase: that phase's slice of one quiet tick of the HN keyword watcher. Illustrative data.

LOAD

Reads the config, the process's memory, and today's spend and tick count. If a hard daily cap is reached, the tick doesn't run and the trace says capped.

config      v3
memory      seen_titles (20)
today       $0 · 12 ticks · no cap reached

OBSERVE

Runs every source and transform in dependency order: HTTP, REST with pagination, RSS, HTML selectors, MCP tools, webhooks, Relay signals, then count, filter, aggregate, compute, extract, sort, sample, format. A failing source is recorded, not fatal.

sources.hn        rest_api → 20 items
transforms.titles extract  → 20 values
source_errors     none

FILTER

The fast path decides if anything happened: a diff against memory, a threshold, a pattern, new items, or always. The very first look at a list is a baseline. It remembers and doesn't alert.

fast_path_diff    triggered: false
diff              []

REASON

Only if the gate fired. A model answers into a JSON schema you declare, on the tier you choose (economy, standard, premium), within a daily dollar budget. Out of budget, it skips reasoning and says so.

reasoning         not configured
model calls       0

ACT

Actions run in order, each with an if or always. In leashed mode, anything that touches the outside world is queued for approval; memory writes and internal signals still happen.

notify_webhook    skipped: if was false
write_state       ok

SAVE

Memory, stats, cost, signals and the trace are written. Secrets never are.

outcome   idle
origin    scheduler
mode      live
cost      $0

Outcomes, as the journal shows them: idle · reasoned · acted · budget_exceeded · capped · error

THE AUTHORING LOOP

validate → shadow → deploy → run → traces

Your AI gets the handbook the moment it connects. The MCP server briefs it on the rules, so you never paste instructions into chat.

SyscallWhat it does
caedos_validateChecks the synth: every reference, secret, connection and expression, before anything runs.
caedos_shadowOne real tick, actions muted. Evidence, not a grade.
caedos_deployRegisters and schedules it. Side-effecting processes come back leashed.
caedos_runOne tick, now. The leash still holds.
caedos_tracesWhat the world did. queued means held, not sent.
caedos_get → caedos_patchChange a running process from evidence, not from a wish.
caedos_ps · caedos_pause · caedos_kill · caedos_devices · caedos_blueprints · caedos_docsThe rest of the table.

THE LEASH

Autonomy is something you grant, per process.

When a synth includes an action that reaches the outside world (a webhook, an API call, an MCP tool), Caedos deploys it with approval_mode: required. Each outbound action lands in the process's approval queue with a one-line summary of what it would send. Approve or reject it. When you've seen enough, press Allow and it runs on its own. You can put the leash back any time.

  • The queue holds references to secrets, not their values; they're resolved only when you approve.
  • Workspace policy sets the default for every process: leash side effects (default), leash everything, or trust everything.
  • Allow isn't one of your AI's tools. Over MCP it can deploy, run and patch; Allow is a button in your control room. Caedos also listens only on your machine by default, because the local API has no authentication yet. Read the security page →

THE TRACE

Every tick, accounted for.

Each trace records two facts that are easy to confuse:

  • Origin: what caused it (scheduler, signal, webhook, authoring, operator, or honestly unknown).
  • Mode: what it was allowed to do (live, queued, shadow).

It also records what each source returned, what the gate decided, what the model said and cost, what each action did, what memory changed, which config version ran, and any warnings. A classic one: your fast path is comparing a field this source never produces.

Replay: take any recorded tick, edit the config, and re-run it against that day's observations. Nothing is fetched. Nothing is sent.

THE RELAY

Synths that talk to each other.

Each synth runs as a process. A process can publish a signal (team.findings) and another can wake up on it. Split watch, decide and send into separate processes, each with its own budget and its own leash. The control room draws the wiring.

Watch

keyword-pulse

Counts Hacker News mentions every hour. When the count shifts, a model writes a one-line read.

auto · model ≤ $0.02/day

team.findings

Decide

drafter

Turns the finding into a draft post. Your AI writes this one; it isn't a shipped blueprint.

auto

content.draft

Send

courier

Sends each draft to your n8n webhook, which posts it.

required · no model calls

queued waits for your Allow

Read Sam's story →

Illustrative wiring from the draft-waits use case. Boxes and wires follow the control room's Relay view.

WHEN THINGS GO WRONG

Weather is not a bug. Idle is not broken.

  • Weather. Timeouts, rate limits, auth refusals, 5xx errors. The other side said "not now" or "not you". Caedos records it and doesn't pay a model to "fix" a 429.
  • Circuit breaker. Four identical failures in a row (eight, if they look transient) halt the process instead of retrying forever.
  • Bounded repair. A background sweep checks each process's health. For a stalled process it can tune a threshold or regenerate the wiring. The candidate is rehearsed first; if it can't be proven and the process acts on the world, it isn't adopted. At most three attempts, each watched for three ticks, and rolled back if it didn't help. Goals are never rewritten automatically.
  • Autopsy. When Caedos gives up, it writes down why: the symptom, the evidence, every attempt, and what you might try. Optionally it sends that to a channel.

What Caedos isn't

  • Not an agent loop. No model decides what to do next at runtime. The synth does.
  • Not code generation at runtime. Synths are data interpreted by one engine.
  • Not hosted. It runs where you run it.
  • Not multi-user yet. One operator per install.