Karjan documentation
Karjan is a managed platform where you define agents, tools, automations and sandboxes inside isolated projects, and Karjan runs them. What you build on top (research pipelines, batch jobs, monitoring, a news digest) is up to you.
Karjan is a research preview: early software, provided as is. Access is by invite code.
Quick start
- Sign up at karjan.si and redeem your invite code.
- Create a project from a template (Starter, Batch or News aggregator).
- Add an LLM key under Configuration → Model providers (OpenRouter recommended; it also powers web search).
- Click Run an agent, give it a task, and watch the run.
Or from a terminal:
npm i -g https://karjan.si/cli/karjan-cli.tgz
karjan login
karjan init my-project --name my-project --template starter
cd my-project
karjan keys set openrouter sk-or-...
karjan run assistant "Create hello.py that prints the date, run it, and tell me the output" --wait
Concepts
| Concept | What it is |
|---|---|
| Project | The isolation boundary: code, configuration, secrets, sandboxes, drives and runs. Nothing is shared across projects. |
| Agent | A template in agents/<name>.yaml: model, instructions, permissions, limits. |
| Agent instance | A running copy of an agent with its own locked configuration and history. Ephemeral instances do one task; persistent instances keep context across turns. |
| Turn | One wake-up of an instance: LLM and tool calls until it finishes. The unit of configuration, cost and history. |
| Tool | A function an agent can call. Built-in (sandbox, web, messages, notes) or your own TypeScript in tools/. |
| Automation | A function started by a schedule, webhook or manually instead of by an agent. |
| Trigger | A schedule or webhook that wakes an agent or runs an automation. |
| Sandbox | A Linux machine an agent works on. Lasting files live on drives. |
| Environment variable | A secret or setting, scoped to exactly the tools and sandboxes that need it. |
Persistent agents have a default instance that is created on its first message. An idle instance costs nothing: it is only a row in the database until a trigger or message wakes it.
Templates and instances. When an instance starts it gets its own copy of the agent configuration. Later changes to the template don't reach it; the instance shows differs from template and you can apply the template (with a diff) or edit the instance's configuration directly from the run view. An instance tuned live can be exported as a template.
Project files
A project is a file tree. The web UI, the CLI and coding agents all edit the same files; every save creates a new version.
karjan.yaml project settings and limits
agents/*.yaml agent templates
sandboxes.yaml machines agents can work on (optional)
triggers.yaml schedules and webhooks (optional)
tools/*.ts your tools and automations (TypeScript)
package.json npm dependencies for your code (optional)
If only YAML changed, the new version is active immediately and agents pick it up at their next turn. If code or dependencies changed, Karjan installs dependencies and bundles your code in a sandbox first; the version becomes active when the build succeeds, and a failed build leaves the previous version active.
JSON Schemas for all files: /api/schema.
karjan.yaml
name: my-project
default_provider: openrouter # openrouter | anthropic | openai
default_model: anthropic/claude-haiku-5.5
limits:
daily_spend_usd: 10 # the project pauses itself when estimated spend reaches this
max_concurrent_turns: 10
max_concurrent_sandboxes: 10
max_hops: 20 # longest causation chain (agent → agent → …)
max_turns_per_chain: 100
run_retention_days: 90 # optional: delete old tool calls, runs and events
agents/.yaml
description: Reads incoming posts and summarizes them for the editor.
persistent: true # false: one task per instance (default)
provider: openrouter # optional, defaults to the project default
model: anthropic/claude-haiku-5.5 # optional
instructions: |
Read incoming posts, research context, summarize what matters,
send summaries to the editor.
permissions:
- tools.lookup_replies # one of your tools
- tools.web.* # all built-in web tools
- tools.messages.send.editor # may message the editor only
- tools.messages.receive.* # accepts messages from anyone
- sandboxes.research-workspace # may use this sandbox
limits:
max_steps: 50 # LLM calls per turn
max_cost_usd: 2.00 # per turn
per_turn:
tools.messages.send: 5 # at most 5 messages per turn
output: # optional JSON Schema; adds a submit_result tool (used by batches)
type: object
properties: { summary: { type: string } }
required: [summary]
compaction:
threshold: 0.75 # compact history at 75% of the context window
Permission patterns are dotted names. * as the last segment matches everything below (tools.web.*), elsewhere it matches one segment. One mechanism covers tools, sandboxes and messaging. A message is delivered only if the sender may send to the receiver and the receiver accepts the sender. Removing a permission takes effect on the next call; adding one takes effect at the next turn.
Built-in tools
| Permission | What it does |
|---|---|
tools.sandbox.exec |
Run a shell command in the agent's sandbox |
tools.sandbox.read_file, write_file, list_files |
File operations in the sandbox |
tools.sandbox.keep_awake |
Keep the sandbox running for background work |
tools.web.search |
Web search (via your OpenRouter key) |
tools.web.fetch |
Fetch a public web page as text |
tools.messages.send.<agent> |
Send a message to another agent |
tools.notes.read, tools.notes.write |
Persistent notes that survive history compaction |
sandboxes.yaml
workspace:
per_instance: true # every agent instance gets its own copy (default: one shared sandbox)
image: universal # universal | node | python | ubuntu
setup: | # optional, built once into a snapshot and reused
pip install pandas
persist: drive # none: throwaway | drive: files under mount persist | full: whole filesystem persists
mount: /workspace
fork_from: base-data # per_instance only: start each instance's drive as a fork of this project drive
drives: # extra project drives (up to 4 per sandbox)
/data: { drive: shared-data, mode: read-only }
vcpus: 1
idle_minutes: 5 # stop after this long without use
network: open # open | none | { allow: [api.example.com] }
credentials: # header injection: code in the sandbox never sees the secret
- domain: api.github.com
header: Authorization
value: "Bearer ${GITHUB_TOKEN}"
env: [SOME_SETTING] # exposed as environment variables (readable by agent-written code)
A drive has one writer at a time and any number of read-only snapshot readers. An agent runs one turn at a time, so its own drive never conflicts. Anything installed outside the drive is gone after a session; install lasting packages onto the drive (for example a virtual environment under /workspace/.venv). Private network ranges and cloud metadata addresses are always blocked.
triggers.yaml
schedules:
- name: morning-digest
cron: "0 8 * * *"
timezone: Europe/Berlin
agent: editor
message: Write today's digest.
delivery: queue
webhooks:
- name: github
agent: researcher # or automation: on_github
delivery: queue
secret_env: GITHUB_WEBHOOK_SECRET # optional: require Bearer token or HMAC signature
Delivery modes say what happens when the target instance is busy:
| Mode | Behavior |
|---|---|
queue |
Deliver when the current turn ends; queued messages are processed together (default) |
launch |
Start a new instance |
interrupt |
Stop the current turn at the next safe point, then start a new turn |
inject |
Add the message to the running turn; the agent sees it on its next LLM call |
skip_if_busy |
Drop the event (useful for heartbeats) |
Webhook URLs are shown under Configuration → Triggers. POST JSON to them; the body is delivered to the agent or passed to the automation as event.payload.
Writing tools and automations
Tools and automations are TypeScript files under tools/. Import from karjan (install types locally with npm i -D https://karjan.si/sdk/karjan.tgz).
import { defineTool, defineAutomation, z } from "karjan";
export const pushLead = defineTool({
name: "crm.push_lead",
description: "Insert a qualified lead into the CRM.",
input: z.object({ name: z.string(), email: z.string(), score: z.number() }),
env: ["CRM_DATABASE_URL"],
handler: async (input, ctx) => {
// use ctx.invocationId as an idempotency key for side effects
return { ok: true };
},
});
export const fetchTimeline = defineAutomation({
name: "fetch_timeline",
trigger: { schedule: "*/30 * * * *" }, // or { webhook: "name" }, or nothing for manual runs
env: ["X_API_TOKEN"],
handler: async (ctx, event) => {
const posts = [];
await ctx.agents.send("researcher", { posts });
},
});
Grant a tool to an agent with tools.crm.push_lead (or tools.crm.*). Code runs in an isolated tool worker per project, never in the agent's sandbox, with only the environment variables it declares and that are scoped to it.
The context object
ctx.invocationId // stable across retries: use as idempotency key
ctx.caller // { agent, instance, chain, hop } or { automation, trigger, chain, hop }
ctx.env.CRM_DATABASE_URL // only declared and scoped variables
ctx.sandbox.exec("python run.py") // the calling agent's sandbox (tools only)
ctx.sandbox.readFile(path) // and writeFile(path, content)
ctx.agents.send("editor", message, { delivery: "queue" })
ctx.agents.start("reviewer", input) // automations only: tools cannot create instances
ctx.llm.generate({ prompt, model }) // inference on the project's key
ctx.log.info("fetched 50 posts") // shown in the run view
Data read from a sandbox may have been written by the model: treat it as untrusted. Tool calls time out after 120 seconds by default (timeoutSeconds, max 280).
Idempotency. Each LLM call and tool call is a durable step: completed calls are never repeated. A tool that crashes halfway is retried with the same ctx.invocationId, so tools with side effects should use it as an idempotency key.
Batches
Launch one agent over many inputs from Run an agent → Batch, or with the CLI:
karjan batch researcher --inputs companies.jsonl --concurrency 20 --budget 5 --estimate
Each input starts one ephemeral instance (with its own fresh sandbox if the agent uses a per_instance sandbox). With an output schema the agent submits a validated structured result; results are collected into a table you can download as JSONL. A batch keeps the configuration it launched with and stops when its budget is reached.
Messages and loops
Agents message each other with tools.messages.send.<agent>; your code can use ctx.agents.send. Tools cannot create agent instances; new instances come only from triggers, batches, automations and the UI. Every event carries a causation chain id and a hop count; chains stop at max_hops and max_turns_per_chain. Per-turn tool limits bound how fast any loop grows.
History, compaction and changes
Raw history is never discarded. When the context reaches the compaction threshold, older turns are summarized into a derived view and recent turns stay verbatim. Configuration changes apply at the next turn; the history gets a visible marker. When you change an instance's instructions or model you choose what it carries forward: keep the history, rebuild the compacted view, keep only its notes, or start fresh. To apply a change right now: stop the turn, change, resume.
Limits and costs
- LLM usage runs on your own keys; Karjan shows estimates from token counts.
- Compute (sandboxes) is drawn from your Karjan credits; at zero, work pauses.
- Per turn:
max_steps,max_cost_usd. Per project:daily_spend_usd, concurrency caps. The project pauses itself at its daily limit. - Billing lists everything that is consuming or could trigger usage, with a Pause everything button.
- Out-of-credit or revoked LLM keys pause the affected agents with a clear status and an email; rate limits are retried with backoff.
CLI
karjan login | logout | whoami
karjan projects | init [dir] --name n --template t | clone <projectId> [dir]
karjan pull | push [-m msg] | status | validate
karjan run <agent> [input] [--new] [--wait]
karjan prompt <instanceId> <message> [--delivery queue|interrupt|inject|skip_if_busy]
karjan batch <agent> --inputs file.jsonl [--concurrency N] [--budget USD] [--estimate] [--wait]
karjan batch-status <batchId> [--jsonl out.jsonl]
karjan instances | logs | turn <turnId> | stop <id> | resume <id>
karjan env list | set NAME=VALUE [--scope tools.x] | unset NAME
karjan keys list | set <provider> <key>
karjan automation run <name> [--input json] | triggers | trigger pause|resume|fire <trigger>
karjan push validates locally, uploads with the version you pulled, and merges per file if someone changed the project in the meantime. Overlapping edits come back with standard conflict markers; resolve them and push again. You can keep your project folder in your own git repository.
API
Everything the UI does goes through the same HTTP API (Authorization: Bearer kj_... with a token from Settings or karjan login). Main endpoints: /api/projects, /api/projects/{id}/files (GET pull, PUT push with merge, PATCH edit), /instances, /batches, /runs, /triggers, /env, /keys, /sandboxes, /drives, /billing. Incoming webhooks: POST /hooks/{project}/{hook}.
Data and privacy
- Secrets (environment variables and LLM keys) are encrypted with a per-project key and decrypted only at the moment of use. They never enter the model's context or histories.
- Export: Settings → Data export creates a machine-readable archive (project files with version history, instance configurations, raw histories and run logs as JSONL, usage records; variable names but no values) with a download link valid for 7 days. Drives are downloaded separately from Resources → Drives.
- Deletion: deleting a project or your account deletes everything, including drives, snapshots and stored files. Download your drives first.
- Retention: set
run_retention_daysin karjan.yaml to delete old runs automatically. - Infrastructure: Vercel (hosting, sandboxes, drives, storage, workflows) and Neon (database), in the US (iad1). Inference goes to the providers whose keys you add.