← The Karsa Manual

How Karsa works

A few ideas explain almost everything in Karsa. Hold these and the rest follows.

Artifacts

Any piece of data a project can reach is an artifact. An artifact can be an arbitrary file, but most are structured (rows in a SQL DB) or semi-structured (documents — markdown + frontmatter, JSON). Structured data follows a schema — its type.

Collections and resources

Artifacts of the same type are grouped into a collection, and an artifact inside a collection is a resource. A collection is the usual way you see and work with data in Karsa: it holds the configuration for where its resources live (a SQL table, a documents folder, …) and it guarantees every resource in it shares the same schema.

Collections are defined per project — what collections a project has, and how each renders, lives in the project's own files, not baked into Karsa.

Every resource looks the same

Whatever the data source, every resource carries the same baseline fields:

"By whom" is an actor: a worker (an agent) or user. That small fact is what lets an agent safely react to changes without reacting to its own changes — the basis of a stable agent loop.

An id reads {type}_{unique} — e.g. spec_01KVSRJ0RG2G. Some resources also have a slug for readability, and you'll see the {id}_{slug} form in URLs and links — but Karsa resolves links by the id alone, so a slug going stale never breaks anything.

CRUD and events — the same for everything

Karsa gives every configured collection a uniform CRUD API — create / read / update / delete — that works out of the box for SQL- and document-backed collections (and takes a custom handler when a source needs one). Every write automatically emits an event: {collection}.created, {collection}.updated, {collection}.deleted.

All events flow onto one queue. You can filter that queue and subscribe to it; when a matching event fires, Karsa notifies your subscriber.

Subscribers — where work happens

A subscriber binds a trigger to a fn: when this trigger fires, run that fn. The trigger is either an event match (e.g. msg.created where role: user) or a time (at / every) — an event-driven subscriber and a scheduled job are the same object, just a different trigger. There is no separate "event handler" or "scheduler" object to create. A fn is the unit of logic that runs — plain code, or an agent-CLI wrapper; a role fn reads its instructions from an agent profile passed in its params, so behavior is data you edit, not code. (See Automating with events, subscribers, and fns.)

Putting it together

Typed collections + a uniform CRUD surface + one event queue + subscribers that bind triggers to fns is enough to stand up a long-running agent doing real work: it reacts to data changes and to the clock, writes typed resources back, and those writes are themselves events that drive the next step. The chat loop you use day-to-day (see Using Karsa) is the first worked example of exactly this.

(Coming: the per-project Karsa app — each project as a self-contained, caged app over the Karsa SDK, not just data + subscribers.)