Quickstart: Build a Company with CEO, CTO, PM & Engineer
This guide brings up a four-agent company on your machine, with no external integrations — just the engine, its infrastructure, and an LLM key. At the end you’ll watch an agent run its first turn on the dashboard, then wire in real integrations one at a time.
Crewlet uses a two-tier config:
- Tier A — ops-owned
crewlet.yamlon disk (where this node’s stream, store and leases live, API host/port/auth, logging — including where it writes). Restart to change. - Tier B — founder-owned
company.yaml, imported into the store (everything else: identity, roles, units, LLM providers, MCP servers, integrations, budgets). Versioned and live-editable.
Prefer not to write the YAML by hand? An AI assistant can interview you and author both files, checking its own work against the shipped schema — see Authoring with an AI assistant, which walks the same ground step by step. This page builds the config manually, which is the better way to learn what the fields mean.
0. There is no infrastructure to start
Section titled “0. There is no infrastructure to start”Crewlet is one binary. Its event stream is a NATS JetStream server it embeds, and its database is a local file it creates — so a company runs with no broker to operate and nothing to point a DSN at. Both slots take an external address when a deployment outgrows that; see Deployment.
The compose file in a repo checkout is for the integration loops (Mattermost, GitLab) further down this page, not for the engine.
1. Write the Tier A bootstrap (crewlet.yaml)
Section titled “1. Write the Tier A bootstrap (crewlet.yaml)”logging: level: debug # every subsystem's DEBUG lines, in colour when you # are watching a terminal. Drop the block (or set # `info`) once the company runs format: console # console (default), text, json file: # optional, and worth it even here: a durable copy path: "./acme-data/crewlet.log" # IN ADDITION to what you are watching. # It rotates itself at 100 MB, keeping five files, # so scrolling past the interesting line costs # nothing. See Deployment → Logging
stream: type: embedded # a JetStream server inside this process: no # listener, no port, no service to operate store_dir: "./acme-data/stream" # REQUIRED. Leave it empty and the # stream is in-memory, so nothing published # survives a restart — including the people # who sign in, whom every node keeps on a # state log whatever backends the company # names, and its items and pages, which are on # the engine's own tracker and knowledge base # here. The engine # REFUSES to boot without one rather than lose # them at the first restart
store: path: "./acme-data/acme.db" # this node's own database, owned # EXCLUSIVELY by this process. Not a shared # database and no DSN: two engines pointed at one # path corrupt it. A second file is created beside # it for the replicated estate — a snapshot is a # copy of one of the two, which is why they are # not one file. See concepts/architecture.md
coordination: type: local # a single node holding its own seat leases; # a fleet needs embedded-kv (see guides/fleet.md)
api: host: "0.0.0.0" port: 8000 # a port > 0 makes `crewlet run` serve the API EMBEDDED in # the engine process (dashboard + webhooks included) — one # process is the whole stack. (Any free port will do; pick # one nothing else on the host has already taken. Port 80 # is worth the privileged bind only once an external # service registers a webhook URL against this engine — # see guides/deployment.md.) external_url: "http://localhost:8000" # REQUIRED once a port is set. Where a BROWSER reaches # this deployment, which is not what the two lines above # bind: its scheme decides the session cookie's flags and # its host is the origin every write is checked against. # Behind a proxy, the public address. auth: totp: optional # people sign in with a login and a password; unset, a # second factor (an authenticator app) is REQUIRED too. # `optional` is fine here because the browser reaches # this on loopback; leave the line out anywhere else. # Nobody holds a password yet: the token below invites # the first person onto their seat (step 4). admin: serve # whether THIS node grants admin to those who hold it: # `serve`, or `refuse` to clamp admin away here and keep # every app. Required once a port is set. tokens: # at least one is REQUIRED once a port is set: every route # needs a credential, and this is also what puts the # first person on their seat on a fresh deployment. - id: founder token: "${CREWLET_API_TOKEN_FOUNDER}" # 26 characters at minimum grants: [admin] # what this credential may do — required and # non-empty: admin, tracker, knowledge, # artifacts. Admin runs the company and # includes every app.
secrets: # REQUIRED. Every record on every state log — the # tracker's, the knowledge base's — is signed # under this keyring, because the broker has no # auth of its own and a record is whatever the # next node applies. `crewlet secrets keygen` # mints one and prints the export line active_key_id: "2026-01" keys: - id: "2026-01" material: "${CREWLET_SECRET_KEY_2026_01}"2. Write the Tier B company (company.yaml)
Section titled “2. Write the Tier B company (company.yaml)”name: "Acme AI"mission: "Ship AI-powered products fast"
policies: - "All features need PM sign-off before development starts" - "Communicate decisions in writing"
providers: llm: default: type: anthropic model: claude-sonnet-5-5 api_keys: - "${ANTHROPIC_API_KEY}" embeddings: type: openai model: text-embedding-3-large # the model decides the vector width # (3072 here), so there is no # `dimensions` to set api_key: "${OPENAI_API_KEY}" # used by the agent-learning subsystem # (diary vector search + episode recall)
# Org-wide roles — these sit above all teams and manage team leads.roles: # You, in the chart. A `kind: human` seat is addressable but never # spawned (no runtime, no inbox, no LLM) — it gives escalation a person # to stop at, and lets agents recognise your activity on the surfaces # you connect later. It is also the seat you join on in step 4: every # person holds a human seat, so a company with none admits nobody. # `contact` is optional — without one you are reached through the # dashboard only, and agents cannot @-mention you on chat; scope # `manages` to the top seat so you aren't copied on everything. - name: Your Name handle: founder # see the note under this block — set these now kind: human manages: [ceo] # BY HANDLE. A `manages:` entry names a seat by # its handle and a unit by its key — never by a # display name, which is prose you will edit contact: # One identity per surface you connect. Swap this for # `slack_user_id` (a `U…` member ID) if Slack is your chat. mattermost_user_id: "${MATTERMOST_FOUNDER_USERNAME}" # your chat username
- name: CEO handle: ceo goal: "Set product vision, prioritize initiatives, and make final calls" backstory: "Experienced founder who balances speed with quality" manages: [cto, pm] # A zero-integration way to see your first agent turn: a scheduled task. # Delete this once you have real integrations delivering work. schedules: - name: hello-crewlet cron: "*/5 * * * *" task: "Write a short status note on what the company should focus on this week."
# Flexible org structure — use any nesting depth and unit types.units: - name: Product Management id: pm-team # the unit's KEY: what a `manages:` entry and a # root seat's `unit:` resolve. The name above is # display. Leave it out and one is minted from # the name at import (`product-management` here) type: team lead: pm # the lead seat's HANDLE purpose: "Define what gets built and why" # A unit's identity on the tracker and the knowledge base. Both default # to the engine's own backends, so these two keys are all it takes to # give the team somewhere to file work and write things down — no site, # no project to create, no per-seat account. They name a Jira project and # a Confluence space just as well if you switch the backend later, which # is why they are not called `jira_project` / `confluence_space`. project: PROD space: PROD roles: - name: PM handle: pm goal: "Turn business goals into clear specs and prioritized backlogs" backstory: "Data-driven product manager who writes crisp requirements" manages: [eng]
- name: Core Engineering id: eng-team type: team lead: cto purpose: "Build and ship the product" project: ENG space: ENG goals: - "Ship MVP in 4 weeks" - "Maintain test coverage above 80%" roles: - name: CTO handle: cto goal: "Set technical direction, make architecture decisions, unblock engineers" backstory: "Senior architect with deep distributed systems experience" manages: [eng]
- name: Engineer handle: eng goal: "Implement features, write tests, and ship quality code" backstory: "Full-stack engineer who writes clean, tested code"Set handle now, and keep it. The handle is the seat’s identity twice
over: an agent’s durable id is a UUIDv5 over "<company name>:<handle>"
(org.DeriveAgentID), and it is also what every lead: and manages: in
the chart above resolves. So changing a handle, or the company name,
mints a new id and orphans that seat’s diary, onboarding markers and
counterparty profiles. It keeps working, but it has lost its memory.
A seat that leaves handle unset gets one derived from its name, and
every write stores it — so an edit of the stored company (the org
builder, PATCH /config, the per-entity routes) keeps it when a name is
corrected. A file does not: every import of one (crewlet config import,
crewlet run -import-company) mints a missing handle afresh from the name
the file holds, so correcting a name in a file
that leaves the handle out replaces the seat. Declare handle on every
seat, and id on every unit, in any file you keep. With its handle
declared, a seat’s name is display and referenced by nothing, so renaming
one is free. See
Agent Runtime.
LLM options
Section titled “LLM options”The providers.llm map takes named provider entries (roles select one via
role.llm; default is the fallback). Three provider types are built in:
providers: llm: # Anthropic (official SDK) default: type: anthropic model: claude-sonnet-5-5 api_keys: ["${ANTHROPIC_API_KEY}"]
# OpenAI (official SDK) gpt: type: openai model: gpt-4o api_keys: ["${OPENAI_API_KEY}"]
# ANY OpenAI-compatible endpoint — a hosted aggregator (OpenRouter, # Together, ...), a cloud provider's OpenAI-compatible gateway, or your # own vLLM / LiteLLM deployment. hosted: type: openai-compatible model: "your-provider/model-id" base_url: "https://api.your-llm-provider.example/v1/" timeout_seconds: 300 # large models can be slow; default 120 api_keys: ["${LLM_API_KEY}"]
# A coding CLI you already subscribe to (Claude Pro/Max, ChatGPT # Plus/Pro, Google AI Pro, a Copilot or Cursor seat) — no API key. # The CLI must be installed on the machine running `crewlet run`. subscription: type: cli-agent model: sonnet # whatever the CLI's --model accepts # (opencode: <provider>/<model>) cli: agent: claude-code # or codex | gemini-cli | opencode # | muse-code | kimi-code | hermes # | pi | ...A cli-agent entry is authenticated once, on the engine host, and then
verified:
crewlet llm login subscription -capture-token # or plain `login` for the # vendor's browser flowcrewlet llm doctor subscriptionEach seat gets its own isolated CLI home, so agents never inherit one another’s sessions or memory — that and the auth options are covered in Subscription LLM Backends.
Useful knobs: on an API entry (anthropic, openai, openai-compatible)
api_keys accepts multiple keys (the provider rotates on
rate-limit/auth errors) — a cli-agent entry takes one, and only under
cli.auth.mode: api-key (see what a cli-agent entry
refuses) —
reasoning_effort sets how
hard the model thinks (low to max; an anthropic entry thinks on every
call and defaults to high, an openai one sends it once reasoning: true
— see Claude models
for what each Claude model accepts), and different
roles can use different entries (e.g. executives on a frontier model, junior
agents on a cheaper one). A role’s llm also accepts a list —
llm: [subscription, default] runs on the flat-rate CLI and falls through
to the metered key when the subscription window is spent. Embeddings (providers.embeddings) power the
agent-learning subsystem and accept any OpenAI-compatible embeddings endpoint
via base_url.
Token budgets (optional)
Section titled “Token budgets (optional)”Control costs with ceilings at the org and/or per-agent level, one per
calendar window on the company’s clock — day, week and month, each
optional:
token_budget: # org-wide, every seat together day: 500000 # a runaway loop burns at most a day's allowance month: 8000000 # the bill
units: - name: Core id: core type: team lead: cto roles: - name: CTO handle: cto token_budget: {day: 100000} # this seat alone, on top of the org's - name: Engineer token_budget: {week: 250000}A day runs from local midnight to midnight, a week is the ISO week from
Monday, a month the calendar month, and each window opens again on its own
when it turns over. Leave a key out for no ceiling on that window — 0 is
refused rather than read as unlimited. When a round would take a window past
its ceiling, the agent’s turn stops immediately and a budget_exhausted event
is emitted, naming the window and when it resets. Until then the agent’s new
messages wait on its inbox rather than being lost, and they are delivered when
the window turns over — or at once, if you raise the ceiling.
Usage is durable — it lives in the fleet’s coordination store, so it survives restarts and is one number for the whole company however many nodes run it. Read it against a running node:
crewlet budgets show # every scope's day, week and month, spent and cappedThere is no reset: a window’s allowance comes back when it turns over, and
room before then is made by raising its ceiling through /config — a seat’s
with PUT /config/roles/{handle} (see
Budgets and spend).
3. Run it
Section titled “3. Run it”crewlet secrets keygen -key-id 2026-01 # prints the export line belowexport CREWLET_SECRET_KEY_2026_01="<the base64 key it printed>"export CREWLET_API_TOKEN_FOUNDER="$(openssl rand -hex 32)"export ANTHROPIC_API_KEY="sk-ant-..."export OPENAI_API_KEY="sk-..." # embeddingsexport MATTERMOST_FOUNDER_USERNAME="you" # your chat username (the human seat)Validate the Tier B YAML first (this reads no environment — ${VAR}
references are checked as references, so it works before any secret is
exported):
crewlet validate # both tiers, from ./crewlet.yaml and ./company.yamlOne command — migrate the store, seed the Tier B company and run the engine in a single invocation:
crewlet run crewlet.yaml -company company.yaml-company is a bootstrap seed: it is imported when the store holds no
company yet, and once one exists it is ignored — with a warning that says so,
naming the file and the two ways to apply it. So a restart never reverts a
change you made live, however stale the file on disk is.
When you do want the file to win, that is a different flag:
crewlet run -import-company company.yaml makes it the active revision over
whatever the fleet is running. And to change a running fleet with no
restart at all, use crewlet config import company.yaml — it goes through the
node’s API and every node converges on it.
A running node always serves the store, not the file.
Or two steps — import once, then run:
crewlet config import company.yamlcrewlet run # boots from the storeBoth flags default to files in the working directory — crewlet.yaml and
company.yaml — so a node whose files are named that way needs neither.
Starting: the port answers within a second of crewlet run, before the
node is ready to serve. While it boots, curl -s localhost:8000/health says
"status": "starting" — 503 while the node opens its store and broker, 200
once it is bringing up the company’s tools — and every other route answers
503 starting with a Retry-After; the dashboard is there once the log says
api_listening and /ready answers 200. See
While a node is starting.
Stopping: press Ctrl+C once for a graceful drain. Running agent turns
finish, and the HTTP surface stays up until they have: the dashboard shows the
node draining, /health stays 200 and /ready answers 503, while every
route that would start new work (a webhook, a config write) answers 503
naming the drain, so nothing new arrives while it converges. The listener
closes once the drain completes. Press it a second time to exit at once;
the first press hands signal handling back to the OS precisely so that works.
There is no third tier. See
Graceful shutdown. Want to
keep the drain’s log? The logging.file block above already has it: the
engine writes the file itself, so the drain is recorded whatever happens to the
terminal. Piping instead? Use tee -i (crewlet run 2>&1 | tee -i run.log),
because a plain tee dies on the first Ctrl+C and the drain’s log has nowhere
to go.
4. Sign in, and invite yourself
Section titled “4. Sign in, and invite yourself”Open the dashboard at http://localhost:8000/. Opening it means signing
in: every route needs a credential, reads included, so the first thing it
shows is its sign-in screen. People sign in with a login and a password, and
nobody has one yet — nobody has joined — so the screen says so and opens its
API token form (on a deployment somebody has joined, it is behind Use an
API token instead). Paste $CREWLET_API_TOKEN_FOUNDER: it is
exchanged for a one-hour session, and the browser keeps the session’s cookie
and never the token, which is sent once, in a header, and held nowhere
afterwards.
Invite yourself onto your seat. Every person holds exactly one human seat
for as long as they exist, so you join the company on the one company.yaml
declares for you, the human seat with the handle founder. Open Company ›
Org chart: your seat’s card (under the name you gave it) reads Open seat,
because nobody holds it yet. Click the card to open the seat beside the
chart: its foot offers Invite and Add person (the same two are on
the card itself, as icons that appear when you point at it or in its menu
from the keyboard, and on the seat’s page). Press Invite — the dialog is
for that seat and offers no other — and fill in
- your email address — what the invitation is held against;
- your access — tick Admin, which ticks and locks both apps (Tracker
and Knowledge) beside it. The founder token holds
admin, and nobody can confer a permission they do not hold. Keep Admin, or nobody after you can be invited or created except with the token again.
The link it shows is the credential and is shown once — copy it now. The engine sends no mail; for anybody else you invite, getting it to them is yours. Until the link is redeemed, the open invitation holds the seat: the card reads Invited · your address, and nobody else can be put on it.
(Add person is the other way onto a vacant seat: it creates the person active at once and shows a one-time password link that sets their first password. For yourself the invitation is the shorter road, since redeeming it signs you in.)
Open the link (this browser is fine). It is the dashboard’s invitation
screen: it names who the invitation is for and the seat it binds — and who
sent it when a person did; this one was issued under the founder token, a
machine nobody holding the link would recognise, so it names no sender — and
asks for a login — it proposes one from your address, dotted like
jane.doe, how you sign in beside your address — your name, and a
password of at least twelve characters (api.auth.min_password_length raises
that). Redeeming it puts you on the founder seat and signs you in: from your
first sign-in you act as that seat. The secret after the . never leaves the
page in a URL: the link carries it in the fragment, which no browser sends to a
server, and the screen sends it in a header and a body instead.
This walkthrough’s crewlet.yaml says totp: optional, so a password alone
signs you in here. Leave that line out on a deployment reached off
loopback — unset, a second factor is required, and the session a redemption
or a password sign-in opens for somebody holding none may only enrol one: the
dashboard shows a QR code to scan with an authenticator app on your phone
(with the key to type, and the otpauth:// link for an app on the same
device, beside it), takes the first code the app shows, and shows the
recovery codes once. See A required second factor
is enrolled before anything
else.
From now on the sign-in screen takes your login or address and your password. Your name at the sidebar’s foot opens your own Account — your password, two-step verification (an authenticator app, which you can set up there even where it is optional) and recovery codes, where you are signed in and your personal access tokens. The menu beside your name holds the same link and signing out, here or everywhere at once. A gesture that changes who may do what, or how somebody proves who they are, asks you to confirm your password first, in one dialog, and then carries on. The founder token stays: it is what a pipeline uses, and the way back in when nobody who can sign in is available — a machine credential, not how people should be signing in. Everybody after you arrives the same way, on a seat of their own: add a human seat for them (Org chart › Edit org › Add human seat), and once it is saved an administrator invites them onto it, or creates them on it, from its card in the org chart — with both apps ticked by default and Admin not. A person handed no permission at all still sees the company’s overview: the org chart, what the agents are doing, spend and schedules — just not its tracker or its knowledge base.
From a shell instead, crewlet iam invite issues the same invitation
through the running node, authenticating with CREWLET_API_TOKEN like every
command that talks to one, and prints its link once. -seat is required —
every invitation names the seat its redemption binds — and crewlet iam seats -unheld lists the human seats nothing holds yet:
export CREWLET_API_TOKEN="$CREWLET_API_TOKEN_FOUNDER"-grants admin stores admin and every app beside it; -grants takes
admin, tracker, knowledge, artifacts or none, and left out on an
invitation it sends every app and not admin.
invitation 0192f4c8-…http://localhost:8000/dashboard#/invite/0192f4c8-….kR3…
expires 2026-06-21 12:00This link is shown once. Send it to them yourself; Crewlet doesn't send email.5. Watch the first turn
Section titled “5. Watch the first turn”It lands on Home, which is what a person opening this wants first: how the company is (who is working, what waits on you, the work in progress and finished, the tokens spent), the decisions only you can make — answered right there, an option of an agent’s question is a button — who is working now, and what the company has done. With no company activity yet each card says so, and a condition the engine itself raised takes over the sentence under the greeting.
The sidebar reads top to bottom: Home, Inbox and My work; the Company group
(Overview, Policies, Org chart, Schedules, Integrations and Budgets); the
Workspace group (Work, Knowledge, Artifacts and Live); and Settings at its
foot. Each workspace’s sections are tabs in its page header. g then a
letter jumps between them, and ? lists every key.
While an agent is working, the Live row counts the seats at work and a
Running group under the sidebar’s shortcuts (after Projects, Pinned and
Starred) lists up to four running turns. Press one to watch it — its
transcript, with the phase it is on open, or the coding run it is parked on
drawn live above its phases. g r does the same for the one running turn, and
opens Live’s Now running when several are running (or none).
Within five minutes the hello-crewlet schedule fires a TaskAssigned at the
CEO. Live shows it: Now running has the seat working, and Turns
shows the turn as it runs — Execute, then Review, each phase listing the rounds
it took, the tools each round called, and the prompts the model actually saw.
A turn has those two phases: Execute both decides and acts, because the frame
that works out what to do is the frame that does it, and Review then judges
what the round actually landed. On a seat’s very first turn one more runs
before them — Onboarding, where the agent reads its own team’s pages once. A
phase that finishes updates in place rather than moving, so you can read one
while the next is running. Turn Engine is what
each one does.
Follow the turn to Work and Knowledge. Both are the engine’s own —
tracker.backend and knowledge.backend default to native, so your company
has a tracker and a wiki from its first minute with nothing to set up. A seat
files with create_work_item and writes with write_page; a row opens the
item beside the list, and ⌘-click opens its page. Work opens as a list —
one line per item, which reads the same on your first task as on your five
hundredth — and Filter and Display in the bar decide what is on it and
how it is drawn: a board, a table, a calendar or a timeline over the same rows.
Projects beside it is the directory of what your units have declared.
My work and the Inbox answer for you — the person bound to the founder seat — and every rule that asks “do you lead this” is asked about your seat. My work is one tab per claim on somebody’s attention — what they hold, the order somebody put it in, the questions waiting on them and the ones they are waiting on — with every count on the strip, and a band above it naming whose day is on screen. The Queue is the work list narrowed to that person: the same Filter, Display and scope controls, opening grouped by when each task is due; read in priority order, its rows are reordered by dragging them. A question put to them is answered on its row in Asked of me. Signed in with the API token instead, the dashboard says so rather than guessing — a token bound to no seat is an ordinary state, not a fault, and its day is the one kept under its own login. See Humans in the Org Chart.
⌘K (Ctrl+K elsewhere) searches everything and acts on it. Type to find a
screen, a task, a page or a colleague — # narrows to tasks, @ to agents,
> to actions — or paste an event, trace or turn id out of a log to open it.
Type a question of three words or more and pause, and a caller bound to a
seat — a signed-in person, who always holds one, or a directory-bound token —
gets a short answer written from your company’s own pages and tasks, with its
sources and the tokens it spent, charged to the company’s budget. It runs on
the auxiliary model of the seat you act as, so a token bound to no seat is
told so instead. From the same box you
can assign the task you found to an agent, ask an agent about what you typed
(the answer lands in your Inbox) or file it as a task — each made as you, under
your seat when you are signed in as yourself and under the token’s own login
when you are signed in with one that no seat binds,
filed in the project on screen or your team’s, and where neither says, in the
project you pick from the list it offers; an action the authority table would
refuse you says why on its row instead.
Your own AI assistant can read and write the same records over MCP. Point any
client at /operator/mcp with your API token:
{ "mcpServers": { "crewlet": { "type": "http", "url": "http://localhost:8000/operator/mcp", "headers": { "Authorization": "Bearer ${CREWLET_API_TOKEN_FOUNDER}" } } }}It gets the same tracker and knowledge-base tools a seat holds — eighteen over
the tracker and five over the pages — and eleven more that no seat is given:
the saved views, the catalogue write, a person’s own queue, inbox and pins, the
trash, and the board drag. Each is decided by the same authority table a seat’s
calls are, and knowledge search sits beside them, with the gestures a person
makes about the company rather than its work — pausing and resuming a seat,
steering a running turn, answering a coding run’s question. On the
deployment’s token its writes carry the token’s own login, token:founder,
with author kind operator — never an agent’s — so an audit can tell them from
an agent’s. The token cannot write as your seat: a seat is held by one row of
the identity directory, and yours is you. See
the operator surface.
Once you sign in as a person, give the assistant your own token rather than
the deployment’s. It acts as you — your seat, your permissions, its writes carrying
your seat’s handle with author kind human — and every write it
makes also records pat:<its id>, so what the assistant did stays tellable
from what you did. Nobody else can mint it, an administrator included; you mint
it yourself, and the command signs in as you for that one request:
crewlet iam token -login jane.doe -label "my assistant" # asks for your passwordSee machine tokens.
The same picture is available over the API:
curl -s -H "Authorization: Bearer $CREWLET_API_TOKEN_FOUNDER" \ http://localhost:8000/agents | python3 -m json.toolcurl -s http://localhost:8000/healthEvery route needs that credential, reads included — /health and /ready
are probes and are the exception, which is why the second call carries nothing.
That is also why the dashboard opened on its sign-in screen. Reads used
to serve without one, which meant anyone who could reach port 8000 could read
the LLM transcripts on /events; see
Configuration § Auth for what replaced
that posture and why it could not simply be defaulted the other way.
If you skipped the import, crewlet run boots in the unconfigured state
with the API still serving — you can then bootstrap live without restarting.
An unconfigured node is not an idle one: its identity estate runs from boot,
so you can sign in on it with the founder token before any company exists. You
cannot invite yourself yet — there is no seat to put you on, so an invitation
there answers 409 no_active_revision — and the company comes first: import
or create it with your human seat in it, then invite yourself onto that seat
as above. The work tracker’s and the knowledge base’s routes
answer 503 no_active_revision until the first revision arrives. That
revision brings up everything the company needs, the engine’s own tracker and
knowledge base included, with their projects and spaces, and the
code sandbox when it configures
providers.sandbox — under the same API, with nothing restarted:
export CREWLET_API_TOKEN="$CREWLET_API_TOKEN_FOUNDER"crewlet config import company.yamlThe engine holds its store, so the command goes through the node’s API: a
PUT /config of the whole file, which stores it as the company’s first
revision and activates it fleet-wide — exactly what a script driving the API
directly sends (Configure via the API).
Or create the company from the dashboard: open Org chart under Company
and its Edit org button (#/company/org/edit). With no configuration
active it opens on a form that starts the company from a template, has the
engine check it, and creates it with PUT /config. Keep its Add a seat for
yourself ticked:
that seat is the one you then invite yourself onto, from the org chart, as
step 4 describes. The builder reads and writes the company
document, so it needs a session holding admin: the one you signed in with
using $CREWLET_API_TOKEN_FOUNDER carries it, and a person you invite needs
it ticked to build the company — or to lead a unit, whose own team they may
edit without it. The dashboard adds no model provider — Settings › Models &
keys edits one the configuration already declares — so one step stays outside
it. Until it is done the company runs and no agent seat takes a turn; whatever
is sent to a seat waits on its inbox. Add providers.llm afterwards with a
PATCH /config merge patch, as the builder’s next steps show
(The Org Builder), and the
work that waited runs.
Split deployment (optional)
Section titled “Split deployment (optional)”Run the agents as their own node when you want webhooks to keep arriving while you restart them, or the agents on a host of their own. The API, the duties and the company’s data stay on the node you already have; the agents move to a node that holds no data — it keeps nothing that has to outlive it, and restarts in seconds whatever the company’s size.
Two changes to the crewlet.yaml above make it the data node: it becomes a
member other nodes can join, and its leases become the fleet’s rather than its
own.
# crewlet.yaml — the data node, as above plus:node: id: data-1 roles: [data, ingress, workers] # everything but the agentsstream: store_dir: "./acme-data/stream" leaf: host: 127.0.0.1 port: 7422 # where the agents node joinscoordination: type: embedded-kv # leases the agents node can seeThe agents node gets a Tier A of its own. It needs no company.yaml, no API
port and no stream directory — it fetches the company from the data node, and
its store is deleted every time it starts:
# agents.yaml — the agents nodenode: id: agents-1 roles: [seats]store: path: "./acme-agents/agents.db" scratch: truestream: leaf: urls: ["nats-leaf://127.0.0.1:7422"]coordination: type: embedded-kv# and the same `secrets:` block as crewlet.yaml, if you configured one:# this node decrypts the company document and the secret store itselfcrewlet run -config crewlet.yaml -api-port 8000 # terminal 1crewlet run -config agents.yaml -api-port 0 # terminal 2-api-port 0 binds nothing on the agents node. Give it a port of its own
instead (-api-port 8001) when something should probe it: without the
ingress role it serves /health and /ready there and nothing else, and its
/ready turns 200 once it has joined the fleet and been admitted — see
Probes on a node without ingress.
The agents node claims the seats once the data node answers that its copy of the company’s records is level with its logs, and its seats’ tracker and knowledge tools read and write through it. See Running One Agent Somewhere Else for what a node without data can and cannot do.
The data node exposes the same REST endpoints, webhook handlers
(/webhooks/jira, /webhooks/slack/{handle}, /webhooks/github,
/webhooks/gitlab, /webhooks/confluence,
/webhooks/forge), and the /config/* CRUD surface (see
API Endpoints). Mattermost is deliberately
absent from that list — it has no usable inbound webhook, so the engine
holds one outbound websocket per agent seat instead, and its inbound path
does not go through an HTTP route at all.
There is no programmatic setup
Section titled “There is no programmatic setup”The engine has no importable API: every package lives under internal/, so
the CLI, the config format and the wire protocol are the whole public
surface. That is deliberate — it means the two-tier config is the only way
to describe a company, and nothing can drift between what a YAML file says
and what an embedding program set up by hand.
Automating a deployment means driving crewlet and the REST API:
crewlet validate # check both tiers in CIcrewlet config import company.yaml # store and activate the companycrewlet run # start the nodeSee Configure via the API for editing a live company, and Tools & MCP for adding behaviour the agents can reach.
Next steps
Section titled “Next steps”An org with no integrations only reacts to schedules. Real work arrives through external surfaces — pick yours in Choosing your stack (LLM, tracker + knowledge base, code host, chat, sandbox — with the hosted vs self-hosted options for each), then wire them in:
- Connect chat so agents collaborate in channels and you can DM them. If
your company already runs on Slack, use it —
the agents land where the conversations already happen, under the
workspace admin and compliance setup you already have. It needs a public
URL for its Events API and one OAuth Allow click per agent:
To try chat on this machine first, Mattermost ships in this repo’s
Terminal window crewlet slack provision company.yaml -public-url https://your-server.comdocker-compose.ymland needs no account, no inbound URL and no clicks:Running that stack on a remote host rather than this machine? SetTerminal window docker compose --profile mattermost up -d --waitscripts/mattermost-dev-bootstrap.shcrewlet mattermost provision company.yamlMATTERMOST_PUBLIC_URLto the address your browser uses first — see The Site URL. The engine needs no public URL; the Mattermost server still needs to know its own. - Connect a work-item tracker — Jira, or the issue tracker of the code host you already run
- Connect a knowledge backend — Confluence —
so shared procedures surface in the executor’s
## Relevant knowledgeblock; publish version-controlled docs withcrewlet confluence import - Connect a code host — GitLab or GitHub — and enable the code sandbox so engineer roles author real merge requests
- Bring your team in: add a human seat for each person who is to sign in (Org chart › Edit org › Add human seat), then invite them onto it or create them on it from its card in Company › Org chart, every person holds a seat of their own. See Putting a person in the seat
- Fill in the founder seat
you already have at the root — add the
contactidentities for each surface you connect (mattermost_user_id,atlassian_account_id,gitlab_username, …) so escalations land in your DMs and agents recognise your activity - Your config is already encrypted at rest — the keyring every node holds seals the entire company document into the DB as one opaque blob on every write, and that seal is also what lets a peer trust a revision it fetches. See Configuration § Secrets
- Stop exporting a variable per credential — with that keyring in place,
crewlet secrets set LLM_API_KEYputs the value in the encrypted secret store, which the engine consults ahead of the process environment when resolving the${...}references you already wrote. Provisioners can write there directly (crewlet gitlab provision … -secret-store), so a minted credential reaches the engine with no file to source and no shell to be in - Explore the Nimbus example — a seven-seat company on Mattermost, running on a coding-CLI subscription rather than an API key. It is the next step up from this page and still needs nothing but the chat server in this repo’s compose file; every integration above adds on top of it
- Add MCP servers so agents can reach your own systems
- See the full configuration reference for all YAML options
Part of Crewlet. Generated from crewlet/crewlet main at ee70177.