← The Karsa Manual

Automating with events, subscribers, and fns

Read this when the user wants a system that keeps running — a pipeline, a watcher, a nightly job — not a one-shot answer. Don't reach for cron files, a background daemon, or an outside queue. Karsa gives you three primitives: events, subscribers, and fns. Build on them, through $KARSA_API_BASE.

The model in one breath

So a pipeline is: something happens → an event fires (or a timer comes due) → a subscriber runs its fn → the fn's writes fire the next events. That's the whole engine. (Read doc_concepts for the model, doc_data-and-storage for collections.)

> One shape for everything. An event-triggered subscriber and a scheduled job are the same thing — a subscriber whose trigger is an event vs a time. There is no separate "scheduler" object to create and no separate "handler" object: a subscriber points straight at a fn.

Publish an event

Any code (an fn, or the agent during its run) can put an event on the queue:

POST ${KARSA_API_BASE}/event
{ "type": "karsa/deal/enriched", "payload": { "deal_id": "deal_01…", "risk": "medium" } }

Collection writes publish their CRUD events for you — you only publish custom events for signals that aren't a plain create/update/delete (e.g. karsa/deal/refresh_due, karsa/report/ready).

Create a subscriber (event-triggered)

Deals land in a deal collection; enrich each new one. Create a subscriber whose trigger matches karsa/deal/created and whose fn is your agent wrapper:

POST ${KARSA_API_BASE}/subscriber
{
  "frontmatter": {
    "trigger": { "on": { "type": "karsa/deal/created", "attrs": { "updated_by": { "$ne": "$self" } } } },
    "fn": "<the run-agent fn id>",
    "params": { "agent": "<the planner user id>", "instruction": "A new deal was created (see the event). Fetch its public filings, summarize the risk, and PATCH the summary back onto the deal via the API." },
    "run_mode": "running"
  }
}

> Which fn for agent work? Bind the project's run-agent fn (Productive seeds it — slug run_agent; your instructions carry its minted id). It runs an agent USER (params.agent — the record's body is its instructions, its fns: list the harness preference, first installed wins), passes your params.instruction for this fire, and — with params.chat_id — streams the output into that chat authored as the agent. Don't bind a raw harness fn (claude_code/codex) from a subscriber: it won't pick a fallback or detach. > > Report vs work. Bind the PLANNER as the agent and let it decide each fire: reports it answers into the chat; work it turns into a ready task the worker executes — serialized, retried, on the board. The task system stays the single executor for work; a timer is just one more producer feeding it.

Create a subscriber (time-triggered = the "schedule")

A scheduled job is just a subscriber with a time trigger — no separate schedule object:

POST ${KARSA_API_BASE}/subscriber
{
  "frontmatter": {
    "trigger": { "every": "24h", "at": "2026-06-24T07:00:00Z" },
    "fn": "<the run-agent fn id>",
    "params": { "agent": "<the planner user id>", "instruction": "Re-fetch each deal’s latest data, update its row, and note anything material that changed.", "chat_id": "<optional: chat to deliver into>" },
    "run_mode": "running"
  }
}

When the timer comes due, Karsa runs the subscriber's fn directly. (You can have the fn publish a custom event instead of doing the work inline — then a second subscriber reacts to it — but usually the fn just does the work.)

> Deferring a task? Don't hand-roll a wake. In Productive, set the task to status: "blocked" + wake_at: "<ISO>" — the seeded task-defer controller creates the one-shot wake for you and flips the task to ready at that moment (cancel or re-aim by editing the task).

Putting a pipeline together

A deal-flow pipeline is a handful of subscribers:

doc_data-and-storage. A deal collection (docdb or sqldb) — the records (doc_data-and-storage). karsa/deal/created. An event-triggered subscriber on karsa/deal/created — enrich each new deal. every: 24h. A time-triggered subscriber (every: 24h) — keep deals current. undefined. The fn does the fetching/summarizing during its run (an agent exec can use the network) and writes results back as resources — whose writes emit the next events.

Build each piece through $KARSA_API_BASE, put the standing instruction in each subscriber's params, and the system runs itself between your chats.

Where fns live (you rarely author the plumbing)

Anti-patterns