Concentus documentation
What the application does, why it does it that way, and where to click. Written against the app as it ships — where something is a limitation it says so rather than leaving you to find out.
What it is
A desktop app for building and running teams of AI agents.
You draw a flow on a canvas: a trigger, one or more agents, and the tools and data they may reach. Concentus runs it on the Claude login already on your machine — no server to deploy, no database to provision, no API key to paste.
The flow you draw is the flow that runs. There is no configuration file behind the picture that could disagree with it, which is the property that makes a canvas worth having at all.
Install
Windows, macOS and Linux. One installer, nothing else.
Download from the releases page and open it. The app carries its own Java runtime and its own PostgreSQL, so there is nothing to install alongside it.
To run the backend on its own — a server, a container, a machine nobody sits at:
Package managers
Also installable through a package manager — which sidesteps the SmartScreen warning an unsigned browser download gets, because the file never carries the browser's mark-of-the-web:
# Windows — scoop (the bucket lives in the main repository)
scoop bucket add concentus https://github.com/Gergilcan/concentus
scoop install concentus
# Windows — winget
winget install concentus
# macOS — Homebrew
brew tap gergilcan/concentus
brew install --cask concentus
# Anywhere with Node — fetches the right installer from Releases
npx concentus
Updating: winget, brew and the plain installers keep the in-app updater; a scoop install
updates with scoop update concentus instead.
On Windows, SmartScreen warning about the downloaded .exe is normal:
the installer is not code-signed yet, and Windows says so about every unsigned download.
More info → Run anyway proceeds; the package managers above avoid the warning
altogether. The macOS build has no equivalent wall — it is signed and notarized, so
Gatekeeper opens it like any App Store-adjacent app.
The bare backend
java -jar concentus-backend.jar
With no environment at all, that starts an embedded PostgreSQL, generates its own encryption
key, and asks for a first account on http://localhost:8080.
Flows execute through Claude Code on the machine running the backend, or against the Anthropic API with a key. The setup screen asks which, and installs and signs in for you if you pick the first. See Paying for runs.
First run
Create the account that owns the installation.
The first launch shows a setup screen. Create an account with an email and a password, or sign in with Google, Microsoft or Discord if you have registered one — either way, the first identity through the door administers the installation.
Everybody after that arrives as a Viewer. Arriving with a valid company account proves who somebody is, not what they should be allowed to change.
The interface speaks English, Spanish and Catalan. It follows your system language by default; the choice lives next to the theme under Resources → Settings, applies instantly, and belongs to this browser rather than to the installation.
On a deployment nobody sits in front of, set CONCENTUS_ADMIN_EMAIL and
CONCENTUS_ADMIN_PASSWORD before the first launch. Otherwise the window
between the port opening and you reaching it is a window in which somebody else could
claim the installation.
Paying for runs
A subscription you already have, or a key billed per token.
The setup screen asks once, before anything is installed — because the answer decides whether anything needs to be.
Your Claude subscription
The default, and the cheaper one: flows run through Claude Code on this machine, on the
subscription you already pay for. There is no key to look after, and no second bill for what
fits in your plan's allowance for non-interactive Claude Code use — Anthropic meters that
allowance separately from your interactive sessions, the Usage page measures what this
machine has consumed, and a flow can fall back to an API key or a local model when it is
spent. It
needs the CLI installed and signed in once, and Concentus does both from the setup screen:
one button runs Anthropic's official installer, and a terminal opens on the sign-in
itself — not on a prompt where the next thing to know is that you type
/login.
The installed binary is put on your PATH as well, so claude is a command you
have from then on rather than something only Concentus can find. On Windows that is the
per-user Path in the registry, written directly — never through setx, which
truncates a long PATH at 1024 characters and takes half your tooling with it. On Linux and
macOS it is a line in the startup files that exist, written once and marked so a second
install does not add a second copy.
An Anthropic API key
Nothing to install and no sign-in: right for a machine nobody sits at. Paste the key into the setup screen and it is kept in the operating system's keyring, on that machine only. Where there is no keyring — a server with no desktop session — it is a file readable by that account alone, and the screen says so rather than pretending otherwise.
The setup screen is reachable again from Setup… on the Concentus tray icon. A key
already in the environment as ANTHROPIC_API_KEY still works; one saved in the
app takes precedence over it.
The canvas
Where a flow is drawn, and what the drawing means.
Blocks are added from the palette on the left. A new block is placed against the one it attaches to — the chain runs left to right, and capabilities hang underneath — and when exactly one existing block could legally take the connection, the wire is drawn for you.
Selecting a block rings what feeds it and what it feeds, one hop in each direction.
Double-clicking opens its properties; a block focused with Tab opens with
Enter.
Undo, tidy, guides
Every canvas change is undoable — Ctrl+Z /
Ctrl+Y, or the ↶ ↷ buttons — including deleting a block, which
used to be irreversible short of reloading without saving. A burst of typing in an
inspector counts as one step, and a drag as one step, so undo moves in the units you think
in.
⌗ Tidy lays out a flow that grew by hand: the chain left to right,
capabilities hung under their agents — the same shape the canvas draws when it places
blocks for you. A layout you hate is one Ctrl+Z from gone.
While dragging, alignment guides snap a block to its neighbours' edges and centres.
Wires are tinted by the output they leave from: the on error path red and dashed, the else path amber — so at three steps' distance the path of failure and the path of success stop looking identical.
What a wire means
Direction carries meaning, so the canvas refuses a wire that would not: an agent cannot feed its MCP server, and a knowledge base cannot feed a SQL source, because neither has anything to do with what arrives. Only consumers — agents, merges, verifiers — can be on the receiving end of a capability.
Two outputs per block
Every block that executes has a second output below its main one, and the wire remembers which it left from. On an agent, a sub-flow, an API call, a merge or a verifier it is on error: the branch wired there runs only when the block failed, and is handed the failure itself, named by the block that produced it — "Presupuestos failed: the mailbox refused the credential" is something a recovery branch can act on, where "the run failed" is not.
A run whose failure was handled this way completes. Somebody drew what should happen when it goes wrong and it happened; leaving the run red would report an unattended failure for working as designed. With nothing wired to the error output, a failure behaves exactly as it always did.
On a condition the second output is else. The main branch runs when the test holds, the else branch when it does not — one test read from both sides. Before this, "if urgent do X, otherwise Y" took two conditions with opposite tests, and the two could drift until some input fell between them. After a for-each, the else branch receives the rejected items rather than dropping them on the floor.
Keyboard
| Keys | What it does |
|---|---|
Ctrl + K | Command palette — reach any view or action |
Ctrl + C / V | Copy and paste blocks. Selected text wins: the canvas stands aside |
Ctrl + D | Duplicate the selected block |
Ctrl + Z / Y | Undo / redo any canvas change, deletions included |
Delete | Remove the selection (undoable) |
Enter | Open the focused block's properties |
Esc | Close the properties dialog |
Blocks
Every kind of box, and when to reach for it.
| Block | What it is |
|---|---|
| Input / trigger | Where the run's text comes from: typed by you, a schedule, a webhook, or an incoming email. |
| Coordinator | The lead: plans the work, hands it to the agents wired under it and writes the answer. One per flow; the palette says so when a canvas already has one. |
| Agent | A model with a system prompt, an effort level and a token ceiling, doing one job in its own context. |
| Verifier | Reads a result and returns a verdict. Its endpoint is a verdict, not a conversation. |
| Merge | Reconciles what several branches produced into one answer. Reaches MCP like an agent does. |
| MCP server | Tools an agent may call. HTTP servers are proxied; stdio servers are launched by the worker itself. |
| Repository | A git checkout the agent works in — read the code, change it, open a pull request. |
| SQL source | A read-only query whose rows are injected as context before the agent starts. |
| API / endpoint | Any HTTP endpoint as a typed tool — from an OpenAPI document, or from a URL you type. |
| Knowledge base | Documents the agent can retrieve from. See Knowledge bases. |
| Condition | Sends the run one way or the other on what actually came back. |
| For each | Takes a list and runs the same branch over every item. |
| Run another flow | A flow inside a flow. See Sub-flows. |
| Send mail | Sends what reached it by SMTP — the result, or a branch’s — to the addresses you name. Credentials come from the store, never from the flow file. |
| Note | Text on the canvas for the next person. Runs ignore it. |
| Group | A box around blocks so they move, copy and read as one thing. |
Triggers
Four ways a run starts.
- Manual — you type the first message in the console.
- Schedule — a cron expression, built from plain choices rather than typed. The canvas card says it in words: every 15 minutes on working days.
- Webhook — a URL with a secret. Posting to it starts the flow with the request's body.
- Mail — an IMAP folder is polled and each matching message starts a run. IMAP rather than SMTP, because folders, flags and read state are what make "process this once" possible at all.
And without the original's webhook secret. Copying an active schedule would give you two flows firing every morning, which is not what "duplicate" means to anybody.
Sub-flows
A flow inside a flow, wired two ways.
Wire a flow into an agent and it runs first, with the text this run started from, and its answer arrives as context. Wire it out of an agent and it runs when that agent finishes — a hand-off. The drawing says which, so there is no dropdown to disagree with it.
The agent also gets a run_flow tool for the same flow, so it can ask again in
its own words when the first answer raises a question. The two are not redundant: one is
what the flow always needs, the other is what this particular run turned out to need.
A child runs with its own budget and its own permission mode. Cycles are cut by an ancestor chain, and nesting stops at three levels deep.
Independent workers
A process per agent, with enforced tool access.
On the shared-session path a local run is one CLI process for the whole flow, so context folders and delegation rosters are written into each agent's instructions rather than enforced: an agent is told which folders and which agents are its own, and could still reach the others.
Independent workers are the real thing. Each agent gets its own process and its own context window, and reaches MCP through a facade that decides which tools it may call and whether writes are blocked (read-only) or simulated (dry-run) — enforced by the backend on every call, not by asking the model nicely.
One ceiling holds over everything: execution.max-processes (Settings, default
10) caps the total claude processes on the machine, whichever run or fan-out
started them. The per-pool limits multiply — eight runs of four workers would be
thirty-two processes, each hundreds of MB — and this caps the product. A worker that does
not fit waits for a slot, and its run says so; raising the setting frees queued work
without a restart.
What passes between them
Two tools, and nothing else. share_finding tells the other workers something
they would otherwise have to find out for themselves — a fact established, a dead end not
worth repeating, a source that turned out to be wrong. read_findings reads
what they have said. Worth calling before starting anything that sounds like it may already
be done.
Deliberately narrow: short notes, no conversation, and no way to ask a sibling something and wait for an answer. A worker that could block on another worker would be a worker that can deadlock with it. The notes are scratch — they belong to this run, not to the flow — and the merge step reads them beside the reports, because a note is often the reason a report says what it says.
When an agent stops getting anywhere
An agent that calls the same tool with the same arguments and gets the same answer three times running is refused once, and told what it has been doing. The rule is same tool, same arguments and same result: a status poll whose answer changes is never touched, which is the difference between waiting and looping. Failures count too — there is as little to learn from the fortieth identical error as from the fortieth identical success.
It is a speed bump rather than a wall. The count resets after a refusal, so an agent that really is waiting on something is delayed rather than stopped; refusing forever would turn a way to save money into a way to break a run that was about to succeed.
A facade can only mediate HTTP. A profile that withholds a tool from a stdio server withholds the whole server, because nothing can be imposed on a process the facade never sees.
Knowledge bases
Reference material too large to hand over whole.
A knowledge base is a named collection of documents an agent retrieves from. It is distinct from a context folder, and the distinction is what to reach for when: a context folder hands the agent the files themselves — right when the files are the work, like a repository. A knowledge base is for manuals, policies and past reports, where only the passages relevant to the task should be injected.
What can go in
| Source | Notes |
|---|---|
| Text layer. A scan with no text layer goes through OCR if it is installed. | |
Word .docx | Paragraphs and tables. Tables come out pipe-delimited so a quote keeps its columns. |
| Excel / CSV | Sheets as rows. |
PowerPoint .pptx | Slide by slide, numbered, with speaker notes — which is often where the sentence explaining a four-word slide lives. |
| Text, Markdown, HTML | As written. |
| Images | Through OCR, when it is installed. |
| Web pages | By address. See below. |
Whole folders can be uploaded at once, with per-folder exclusions — a real folder always has
a .gitignore or a stray image in it somewhere.
Pages
Add a page takes a URL. The document is filed under that address, so adding it again replaces it rather than filing a second copy — which is what makes Refresh pages mean something: one click re-fetches every page in the base when the wiki moves on. Uploads are untouched by a refresh; a file somebody chose has no address to go back to.
One address is fetched — the links on the page are not followed. A crawler that wanders ingests the whole intranet because somebody pasted a nav page, and then answers questions from the cafeteria menu.
A server that fetches an address you type can reach far more than you can. Loopback,
link-local and cloud-metadata addresses are refused, redirects are re-checked at every
hop, and a host that does not resolve fails closed. Name hosts in
knowledge.allowed-web-hosts to opt into ones that would otherwise be blocked.
Who may read a base
Every other resource here is reachable by anybody who may edit flows, and for an agent definition that is right — it is configuration. A knowledge base is not configuration; it is the documents themselves. So a base can name the lowest role that may read it, on the same ladder everything else uses.
Absent means everybody, which is what every base created before this existed meant and must go on meaning. It is enforced at the API, and again when a run injects a base — against the role of whoever started the run, not whoever drew the flow. A flow outlives its author’s session, and inheriting their reach would make every restricted base readable by anybody who could press Run on the right flow.
Chunking
Documents are split on paragraph boundaries at about 1,500 characters, with 200 characters of overlap so a sentence cut at a boundary still appears whole in one of its two chunks. A single document stops at 2,000 chunks, and says so when it does.
Retrieval
How a passage is found. the important part
Every query runs two searches, and they are fused rather than chosen between.
A vector search cannot pick out an exact token. A document reference, an error code, a filename sits in embedding space beside all of its neighbours, so asking for a policy by its number returns twelve other policies. A keyword search finds it first try. The mirror case is just as common: no keyword search matches "what happens when somebody leaves" against a document titled Offboarding, because they share no words. Running both is not hedging — it is the only way to answer both questions.
keyword (ts_rank_cd, top 50) ─┐
├─ Reciprocal Rank Fusion ─ cross-encoder ─ top k
semantic (cosine, top 50) ─┘
Fusion is by rank, not by score. A cosine similarity and a PostgreSQL text rank are not comparable quantities, and weighting them against each other would mean inventing an exchange rate and then tuning it forever. Reciprocal Rank Fusion asks only how near the top of its own list each branch put a passage — and a passage only one branch found still surfaces, which is the entire point.
The reranker
A cross-encoder then reorders the survivors. It reads the question and a passage together and scores the pair, which is what lets it notice that a passage describes the right process for the wrong department — a distinction two separately-computed embeddings cannot express. Too slow for a whole base; exactly right for the fifty candidates a search already found.
The two models
Both are optional, both run inside the app, and both are downloaded on demand from Resources → Knowledge. No Ollama, no Docker, no server.
| Model | Size | Without it |
|---|---|---|
| multilingual-e5-small embedding | ~130 MB | Keyword search still works; nothing ranks by meaning. |
| bge-reranker-base reranker | ~282 MB | The fused order stands, and the panel says what is missing. |
A model server serving bge-m3 is picked up automatically if you have one.
What reaches the agent
Retrieved passages are assembled, not pasted:
- Merged — adjacent passages are joined without repeating the overlap between chunks, which would otherwise read as emphasis.
- Ordered by document and position, never by score. A document is read forwards; score order hands the model step 4, then step 1, then step 7 and asks it to explain the process.
- Trimmed to a character budget per source (
knowledge.context-chars, 12,000 by default), and the agent is told some were left out. - Numbered with a citation key, and the prompt asks for those markers back. An answer nobody can check against its sources is the failure this whole subsystem exists to avoid.
Asking again
The agent also gets a search_knowledge tool. The preload answers what the flow
always needs; the tool answers the question that only exists because of what the first
answer said.
Measuring retrieval
Because every change to ranking sounds like an improvement.
The usual evidence for a retrieval change is three queries typed after making it. Under that regime a base gets quietly worse while everyone agrees it feels sharper.
So a base can keep a golden set: questions somebody really asks, each with the document that answers it. Resources → Knowledge → Measure this base. Running it gives three numbers, because one number hides the two failures that matter.
| Number | What it means |
|---|---|
| Answered | Share of questions where an expected document made the top 5. How often somebody would have got an answer at all. |
| Documents found | Share of every expected document retrieved. Lower than Answered whenever a question needs two documents and one came back — a half answer the headline would hide. |
| Rank quality | 1.00 when answers come first, 0.50 when second, 0 when never. A passage arriving fifth spends the context budget of four wrong ones getting there. |
It runs the same questions with and without the reranker, which is the only honest way to answer "is it worth 282 MB on my documents" — a benchmark on somebody else's corpus is not evidence about yours. A miss shows what came back instead, because that is the explanation rather than the verdict.
What is measured is whether the right document came back. Grading generated text needs a model to judge a model — a second thing to be wrong, and a bill per run — and it answers a different question: a wrong answer over the right passage is a prompt problem, while a right-sounding answer over the wrong passage is this one, and it is the one that ships unnoticed.
SQL sources
Rows as context, before the agent starts.
A SQL block runs a read-only query and injects its rows into the connected agent's context. Generic JDBC; PostgreSQL is bundled and other drivers can be added to the backend. Test the query from the block's properties before running the flow.
Only a single SELECT is allowed, as defence in depth alongside whatever grants
the database itself enforces. Credentials are referenced by id from
Resources → Credentials, never named inline.
Loopback, link-local and cloud-metadata hosts are blocked by default. Add hosts to
rag.allowed-jdbc-hosts to permit them — except the metadata address, which is
blocked regardless.
APIs and MCP
Two ways to give an agent something to call.
MCP servers
Any Model Context Protocol server, over HTTP or launched as a local process. A catalogue offers the common ones with their setup — the variables they need, the runtime they need installed — rather than a blank JSON box. OAuth servers keep their tokens in one encrypted credential and renew them mid-run when a call comes back 401.
When a server offers a great many tools, the agent gets a search tool instead of a hundred schemas: it asks for what it needs and gets back the few definitions that match.
API endpoints
Paste a URL and it becomes a typed tool — no OpenAPI document required. Placeholders in the path become the arguments, and a body is offered only where one belongs. With an OpenAPI document, the operations are listed and you choose which the agent may call.
Runs and the console
What happened, block by block.
Every run records its events, its per-block input and output, its token counts and an estimated cost. A block can be re-run on its own, with exactly the input it was handed — a step that failed at the end of a long run does not cost you the twenty minutes before it — and optionally on a different model.
Two runs can be compared side by side. Runs, events and per-block output are persisted; the live handles to a running session are not, so restarting the backend leaves history intact and drops anything mid-flight.
An estimate at published per-token rates. On a Claude subscription it is equivalent usage against your plan's non-interactive allowance rather than a charge; on the API it approximates the real charge.
Approvals
When a run should wait for a person.
Permission mode is set on the coordinator, because one CLI process runs the whole flow: bypass (the only mode that works unattended), plan only, and ask me to approve. A run waiting for an answer says so, and can be answered from the console — or from Slack or Teams, in the thread the notification arrived in.
A run that has already handed its result on is closed to further messages.
Versions and golden runs
History that is append-only, and a reference to drift from.
Every save appends a revision, credited to whoever made it. History is reachable from the dashboard's History: preview a revision on the canvas, or restore it — a restore is appended as a new revision rather than erasing what it replaced, so the thing you restored over is still one click away.
A run can be marked as a golden run: a reference for what this flow should produce. Later runs are compared against it, and a flow whose output has drifted says so on its card.
Replay against the current flow
Golden runs compare outputs; debugging usually needs to compare decisions — with the same input, where does the path diverge? From any execution, ⟲ Replay vs current walks that run's recorded per-block outputs through the flow as saved today, without running anything, and paints the answer on the canvas: an amber ring on every block whose fate flips (it ran and would now be skipped, or the reverse), a chip saying which way and why, and the blocks that ran but no longer exist named in the banner.
It replays routing, not agents — gates decide from recorded text, so their answers are exact, but where a decision would need output that was never recorded, the block says cannot be decided rather than guessing.
Marketplace
Where the things a flow is built from are shared and installed.
A view of its own, beside Studio. People publish MCP servers, library agents, facades, skills, plugins, API definitions and flow templates; everyone else installs them into their own organization with one click, and a flow uses them directly. Search is instant and local; filters by kind, scope, state and tag; sort by installs, newest or name. Every card is one line and a state chip — Installed, Update, Pending — and the explanation lives in the tooltip.
Two scopes
An item published to this organization is visible to its members and needs
no approval — any member may publish. An item published globally is visible
to every organization on the deployment once a curator approves it; until
then only its author and the curators see it, marked pending, and a rejection carries a
sentence the author reads. The curators are the administrators of the organization named by
the setting marketplace.curator-organization — by default the oldest one, the
author's own. On a deployment with a single organization the scope control is not shown.
What install creates
The resource the item describes, in your organization: an MCP server definition (with its credential slot empty and named), a library agent, a facade profile, a skill, a plugin install, or a flow (paused, without secrets, like a duplicate). An API definition creates nothing: the API block's inspector offers Use from Marketplace… and fills its fields. Credentials never travel — publishing a resource strips them and says what it dropped. Resources that came from an install carry a Marketplace · vN chip and offer Update when the author republished; uninstall removes what the install created.
What is there on day one
The app seeds its own library as built-in items: the MCP catalogue (Linear, GitHub, the Google and Microsoft servers, …), the four library agents and the eight starter flows. A built-in cannot be edited; an update to the app re-seeds it, and one you deleted stays deleted. Publishing, approving, rejecting, installing and uninstalling are written to the audit trail.
Teams and sign-in
Who may change what.
| Role | May |
|---|---|
| Viewer | Read flows and history. |
| Operator | Run flows and answer approvals. |
| Member | Create and edit flows and resources. |
| Admin | Everything, including members, settings and storage. |
Enforced on every request by both method and path — not only by what the interface chooses to draw.
Signing in with a directory
Microsoft, Google and Discord are registered from inside the app, under Resources → Members → Sign-in providers, and take effect immediately. The sign-in screen shows all three whether or not they are configured: an unregistered one says what it needs rather than sending somebody to a provider that will refuse them after they have typed their password.
People are matched by the provider's own immutable id, never by their address — addresses are reassigned when people leave, and matching on one would eventually hand a leaver's flows to whoever inherited the mailbox. An address that already has an account is linked to it rather than duplicated.
Every registration fails the same way the first time. Copy it from the providers screen —
it is computed from the request you are making — and register exactly that string. On
Microsoft, the field to put it in is web.redirectUris in the manifest, because
the portal refuses http:// with a loopback address.
Several accounts at once
One browser can hold several signed-in accounts and switch between them, with each one's role shown beside it — which is also the practical way to check that a permission actually holds. Two accounts active at once needs two windows, because a session is a cookie.
Groups
An organization can be split into groups — a platform team, a support crew, a client's squad — under Resources → Groups. Two things follow the group: a resource (a flow, an MCP server, an agent, a facade, a knowledge base, a credential, …) can be visible to one group instead of the whole organization, chosen with Visible to on the resource or the flow card, and seen by the group's members and the administrators only; and a group carries its own policy and its own overrides of the settings that apply per run, layered over the organization's for the flows it holds. Administrators create groups; a group's managers add and remove its members and edit its settings. Deleting a group returns its resources to the organization. The Marketplace can publish to a group and install into one. Enterprise; on a Team license the screen shows why and what a group already scopes stays scoped.
Service accounts
Tokens for machines — a CI job, a cron entry, another system — under
Resources → Service accounts. Each acts as its role on every request and
takes no seat; the token (csa_…) is shown once and stored hashed. The headless
CLI reads it from CONCENTUS_TOKEN and skips the sign-in. A Team license holds
two at a time; Enterprise, as many as you need.
Several organizations
One deployment can carry several organizations, each with its own flows, credentials, runs and settings, under Resources → Organizations. A person can belong to more than one and switches from the account menu; seats are counted per person, not per membership. Creating the second organization is an Enterprise feature — everything else on that screen works on Team.
Organization policies
Rules over every flow in the organization, under Resources → Policies: a default facade for independent workers (filled in where a block names none, or refused), a ceiling on the permission mode any block may ask for, a monthly budget for the whole organization beside each flow’s own, and approval before a published endpoint answers — until an admin approves the saved token, the endpoint answers the same 404 as an unpublished one. The flow doctor reports all three before a run. Enterprise; a Team deployment sees what is stored, read-only.
Audit trail and retention
Who did what, and when — runs started and stopped, approvals, flows saved and published,
members and roles, credentials (label and kind only), settings (key only), license
installs, backups — under Resources → Audit, readable by administrators on
every tier, with actor, kind and date filters. Exporting it as CSV or JSON is Enterprise.
Retention follows the tier: a Team deployment keeps ninety days of runs, flow versions
and audit rows and purges nightly (golden runs and every flow’s current version always
stay); Enterprise keeps everything unless retention.enterprise-days says
otherwise; a free installation is never purged.
Runners
Execution on a machine somebody operates, for a hub somebody deployed.
The desktop app is one backend that is both the control plane and the place flows execute:
the claude CLI runs on the machine the window is on, against the login already
there. A runner is that execution half on its own — a process on a machine
you operate (a laptop that stays on, a NAS, a container on your own cluster) that connects
out to a Concentus backend, the hub, registers with a token minted
there, and runs the CLI turns the hub hands it. The hub keeps flows, runs, approvals,
credentials, groups, policies and the interface; the runner keeps the login, the folders,
the clones and the processes. The app on its own is unchanged — runners are for the shape
where a hub runs somewhere (java -jar, or the Docker image) and is reached in
a browser.
Where the login lives
Anthropic's terms do not allow a subscription's credentials to be routed through a third
party, and a hub holding your claude setup-token would be exactly that. So the
hub never holds a Claude login: a runner is registered by whoever operates it, on a
machine they operate, and carries its own auth — the CLI's login there,
CLAUDE_CODE_OAUTH_TOKEN from claude setup-token, or
ANTHROPIC_API_KEY. The hub only learns which kind it is, and the roster shows
it.
Scopes
Whoever registers a runner chooses who may run flows on it. Organization: everyone in it; an admin registers these. Group: its members, an admin, and a schedule or webhook of a flow that belongs to the group; a group's manager may register one, and it needs groups, so Enterprise. Only me: the owner and nobody else — not an admin who is not the owner, never a schedule or a webhook. It is somebody's machine and somebody's login; any member may register one for themselves. Admins see every runner; everybody else the organization's, their groups' and their own.
Starting one
Resources → Runners → + New asks a name and a scope and answers with the
token (crn_…), shown once and stored hashed, and the three ways to start it
with the hub's URL filled in:
java -jar concentus-backend.jar runner --url https://hub.example.com --token crn_…
docker run -d -v concentus-runner:/data \
-e CONCENTUS_RUNNER_URL=https://hub.example.com -e CONCENTUS_RUNNER_TOKEN=crn_… \
-e CLAUDE_CODE_OAUTH_TOKEN=… ghcr.io/gergilcan/concentus-runner:latest
Or the desktop app itself, from the tray, under Set up… → Server: it keeps
working as it does and also executes for that hub. In runner mode the jar
never starts Spring — no database, no port — and exits 3 on a refused token
rather than retrying. What it needs on its side: git, the claude
CLI, LOCAL_CONTEXT_ROOTS for the folders flows may read there (empty rejects
every one, as on a hub), and EXECUTION_MAX_PROCESSES for how many turns at
once — it enforces its own ceiling, not the hub's. A hub and a runner together, with
compose, are in packaging/docker of the repository.
Which runner a flow uses
Runs on, in the flow's Settings. This server (the default): here when the CLI is logged in on this backend, otherwise the least busy online runner you may use — the log says so — otherwise an API key or the refusal. Any runner: the least busy usable runner that is online; none online and the launch fails at once, there is no queue. A runner by name: that one; offline, revoked or not yours to use, and the launch is refused naming it. Only Claude CLI flows go to runners; a self-hosted model stays on the hub. The run's first line names the runner, the runs list carries a chip with its name, and the run is billed the way a local one is — on that runner's subscription.
A turn does not survive the runner disconnecting: after 45 s without a heartbeat the hub ends it as disconnected, and what the runner was running finishes on its side with its output lost. The runner reconnects on its own; nothing is resumed. Finished workspaces are not cleaned up on the runner, and no queue waits for a runner to come online.
Storage
Embedded by default; a company database when you want one.
Every build stores its data in an embedded PostgreSQL under the app's data directory. That is not a demo mode — it is the same schema, the same migrations and the same code path as a server deployment.
Resources → Storage points the installation at your own PostgreSQL and copies what is already there across, table by table, reporting what moved. The copy is a copy: the embedded data stays where it was until you are satisfied.
The desktop app generates it on first launch and keeps it in your OS keyring; a server
sets CONCENTUS_SECRET_KEY — a vault, a container secret. A database restored
without that key still opens: the credentials sealed under it show as locked,
every flow keeps its references, and you enter each value again. To carry values in a
backup file, an administrator can tick Include credential values on the export.
Updates
A mark in the corner, not a dialog.
The desktop app updates itself: it checks every four hours, downloads in the background, and installs when you quit. None of that interrupts anything, which also made it invisible — so the state now sits in the top right of the window. Silent while there is nothing to say, a green dot when a version is downloaded and waiting. Clicking it says what state things are in and offers the one action that state has: check now, or restart and install.
An update dialog that interrupts work teaches people to dismiss update dialogs. A mark that waits to be noticed costs nothing and is still there tomorrow.
Installing without being asked anything
Pressing install runs the installer silently: no wizard, no licence page you already accepted, and no question about where to put it — it goes where the existing installation already is. The app reopens when it is done.
Before the installer starts, Concentus ends its own backend and waits for it to actually be gone, taking the whole process tree with it if asking politely does not work. That wait is not politeness: the bundled Java runtime lives inside the installation directory, so an installer that starts while the backend still holds those files stops and reports that the application is still open — behind a window that has already disappeared.
Which releases you are offered
A prerelease build follows both prereleases and final releases, taking whichever is newer. A final build follows final releases only: somebody who installed a stable version did not ask to be moved onto a prerelease.
-beta.N, and that is not cosmetic
electron-updater derives the update channel from the version's prerelease identifier, and
only offers a final release to builds whose channel is null, alpha or
beta — those three names are hard-coded. On any other identifier,
rc included, a build can never reach a stable version at all. It must also be
dotted: 0.1.3-beta.1 gives every release in the train the same channel, while
0.1.3-beta1 gives each its own and nobody is ever offered the next one. The
release workflow refuses both mistakes.
A development run has no packaged app to update, and a .deb install is
updated by the system's package manager. Both say so and stand down rather than failing
on a loop.
Settings
The things that used to be environment variables.
Resources → Settings. A value is looked up in three places, in order: what somebody set in the application, what the deployment was started with, then the built-in default. That order is what keeps both audiences working — a container started by a pipeline still takes its environment, and the person in front of the app can change the same thing from a form.
The screen says where each value came from, because "8" means three different things and only one of them is yours to clear, and whether the change waits for a restart — most do, because they size a thread pool or a policy when the application starts. Sign-in providers are the deliberate exception: somebody who has just pasted a client id wants to try it.
Clearing a field removes the override rather than storing an empty one. Secrets are sealed with the same key that protects stored credentials and are never read back out of the API.
Appearance sits at the top of the same screen — dark, light, or high contrast. It is not one of the settings below it: it lives in this browser, applies the instant it is chosen, and has no Save. It used to be a button in the header, cycling one click at a time through three values, which is a lot of the most valuable corner of the screen for something a person sets once.
Traces and metrics
OpenTelemetry, in this application's own units of work.
The framework already instruments what a framework can see. None of that answers the question anybody actually has here, which is about a run. So there are six span names and one vocabulary of attributes:
| Span | Covers |
|---|---|
concentus.run | One run, from its trigger to its last block |
concentus.node | One block: an agent, a gate, a sub-flow, an API call |
concentus.worker | One independent worker in a fan-out |
concentus.tool | One MCP tool call, refusals included |
concentus.model | One request to a model |
concentus.retrieval | One knowledge search: both branches, the fusion, any reranking |
Attributes carry identifiers, counts and outcomes. A trace ends up in somebody else's system, usually with a longer retention than anything here and read by people who were never given access to the flows. What is useful for debugging and what is safe to export are different sets, and where they differ the smaller one wins.
Nothing is exported until a collector is named, and it is off by a switch rather than by a
blank address — an empty endpoint is not "nowhere" to an OTLP exporter, it is an invalid URL.
Configure it under Settings, or with OTLP_ENABLED and OTLP_ENDPOINT.
Traces export on every tier, free installations included; only the metrics export is
an Enterprise feature.
Sending traces to an LLM observability tool
Anything that speaks OTLP/HTTP reads them, and the agent-shaped ones group a run's spans by trace on their own. The endpoint and one header are the whole integration.
| Where | Endpoint | Header |
|---|---|---|
| Langfuse (cloud) | https://cloud.langfuse.com/api/public/otel/v1/traces | authorization: Basic <base64 of public:secret> |
| LangSmith | https://api.smith.langchain.com/otel/v1/traces | x-api-key: <key> |
| Phoenix (self-hosted) | http://localhost:6006/v1/traces | none locally |
| Collector · Tempo · Jaeger | http://localhost:4318/v1/traces | whatever it wants |
The authorization header has its own row in Settings. Any other header is a
Spring property: management.otlp.tracing.headers.x-api-key=….
Headless CLI
Running a flow from a script.
pnpm desktop:build # once — builds the jar
export CONCENTUS_EMAIL=you@company.com
export CONCENTUS_PASSWORD=…
# or, for a machine: a service account token instead of a sign-in
export CONCENTUS_TOKEN=csa_…
node scripts/concentus-run.mjs my-flow.json --input "go"
echo $? # 0 completed · 1 failed · 2 needs a human
It signs in like everything else does. A script that could drive the backend without
credentials would be a hole rather than a convenience. Prefer the environment variables over
--email / --password: an argument ends up in the job's log and in
the process list. A CONCENTUS_TOKEN from a service account skips the sign-in
altogether and acts as that account’s role — the right shape for CI.
Licensing
What is free, what needs a seat, and where a license goes.
Concentus runs with no license installed at all: the embedded database, one seat. That is not a trial — nothing about it expires or stops working, and it is what every install starts on.
What needs no license
Everything an individual uses: the canvas, every block, every trigger, knowledge bases, the local model path — all of it, at the one-seat limit above. An individual license is free and changes nothing functional; it just puts a name on the installation. Request one from the site and it arrives by email.
What needs a team or enterprise license
Three things, all past that one-seat default:
- A shared database — pointing the installation at a PostgreSQL your team connects to, instead of the embedded one.
- Members beyond the first — a second seat and up.
- SSO — signing in with Microsoft, Google or Discord rather than a local password.
A Team license covers up to ten seats — 25 € per seat per month, or 20 € per seat per month when the year is paid at once — and is bought by card on the pricing section: the license arrives by email minutes after checkout, renewing is buying again, and every member pastes the same token. It carries everything a team needs to work together: the three above, the audit trail on screen, service accounts, published endpoints, the Google and Microsoft sign-ins.
Enterprise is scale and governance — any number of seats, an invoice instead of a card, or a contract: write to gila791@hotmail.com. Ten things are Enterprise-only, and each is named the same way on the site, in the License panel and in the sentence you see when a Team license does not cover it:
- Custom identity providers — any OpenID Connect issuer (Okta, Keycloak, a custom Entra app). Team has the Google and Microsoft presets.
- Automatic accounts for an email domain — first sign-in creates the account. Team invites people.
- OpenTelemetry metrics export to your collector. Traces export on every tier.
- Organization policies — see Teams and sign-in.
- Audit trail export as CSV or JSON.
- Unlimited retention — Team keeps ninety days.
- Several organizations on one deployment.
- Unlimited service accounts — Team gets two.
- Published endpoints without a rate limit — Team: 60 requests a minute per token.
- Groups inside an organization — resources visible to one group, and a group's own settings and policy.
Enterprise is also the tier for a deployment behind a proxy or without internet: the license verifies offline, telemetry is opt-in, and a server deployment makes no outside call of its own — Claude itself still needs Anthropic reachable; a flow on a self-hosted model needs nothing outside the network.
To try before either: the trial form on the site issues a 14-day team license for up to ten seats, no card, one per address. It is a team license in every way the app cares about — the Settings screen just counts its days down — and when it ends, the same grace and the same one-seat fallback as below.
Where a license goes
Two sources, first match wins:
| Source | When to use it |
|---|---|
CONCENTUS_LICENSE |
An environment variable. For a container or a scripted deployment with no writable disk to speak of — and it always wins over the file below while it is set. |
license.key |
Beside the app's data directory. What Resources → Settings → License writes for everyone else — paste the token you were emailed and the app writes the file for you. |
Expiry and grace
An individual license never expires — it is free, so there is nothing to lapse. A team or enterprise license that has expired keeps working for 14 more days past its date: renewal is a purchase order, not a click, and a flow that stops running the moment a date rolls over — mid-afternoon, on whoever is on call — is a worse failure than a warning for two weeks.
The installation does not lock up; it drops to the same one-seat, embedded-database state a fresh install starts in, until a renewed license is installed.
HTTP API
What the interface itself calls.
Every write echoes a CSRF token that arrives as a readable cookie on the first GET. A cookie alone would not prove intent, which is the whole point of the header.
| Method | Path | What it does |
|---|---|---|
| GET | /api/account/session | Who is signed in, which providers exist, whether this installation needs its first account |
| POST | /api/account/setup | Create that first account. Refuses once one exists |
| POST | /api/account/login | Sign in · /logout |
| GET/POST | /api/account/members | Organization members admin |
| GET/PUT | /api/account/providers | Register Microsoft / Google / Discord admin |
| GET/POST | /api/flows | List and save flows · /{id}/versions |
| POST | /api/runs | Start a run · /{id}/events streams it |
| GET/POST | /api/knowledge | Knowledge bases · /{id}/documents · /{id}/urls · /{id}/refresh |
| POST | /api/knowledge/{id}/search | Try a query against a base |
| GET/POST | /api/knowledge/{id}/evals | The golden set · /evals/run scores it |
| GET | /api/knowledge/embedder | The optional models · /reranker · …/download |
| GET/PUT | /api/settings | Everything adjustable, with where each value came from admin |
| GET/POST | /api/storage | Where data lives · /migrate copies it admin |
| GET | /api/audit | The trail, filtered and paged · /export?format=csv|json admin |
| GET/POST | /api/retention | The window in force · /run-now purges today admin |
| GET/POST | /api/organizations | Organizations · /{id}/members · /{id}/switch admin |
| GET/POST | /api/service-accounts | Tokens for machines · /{id}/revoke admin |
| GET/POST | /api/runners | Runners · /{id}/revoke · /usable — the ones you may run flows on · /self — this backend's own agent |
| WS | /ws/runner | A runner's connection, with its crn_… token as a bearer |
| GET/PUT | /api/org-policy | The organization’s rules · /publish/{flowId}/approve admin |
| POST | /api/public/flows/{id}/run | A published endpoint, with its bearer token · /runs/{runId} polls it |
| GET/POST | /api/license | The license in force, with what it unlocks · install a token admin |
Environment
Nothing is required. These are the exceptions worth knowing.
Four things cannot be settings, because they are what has to be known before there is anywhere to keep a setting.
| Variable | When you need it |
|---|---|
PERSIST_DB_URLPERSIST_DB_USERPERSIST_DB_PASSWORD |
Pointing at an external PostgreSQL from the start. Otherwise the embedded one is used, and the Storage screen can move you later. |
CONCENTUS_SECRET_KEY |
Sealing stored credentials at rest (AES-256-GCM). Optional: without it they are stored as typed. Installations sharing a database share the key. |
CONCENTUS_ADMIN_EMAILCONCENTUS_ADMIN_PASSWORD |
Bootstrapping the first account on a deployment nobody sits in front of. |
APP_DATA_DIR · SERVER_PORT |
Where data lives and which port to bind. |
CONCENTUS_RUNNER_URLCONCENTUS_RUNNER_TOKENCONCENTUS_RUNNER_NAME |
Starting the jar as a runner (with runner as its first argument), or making a full backend also act as one. |
CONCENTUS_RUNNER_DATA_DIR · CLAUDE_COMMAND · LOCAL_CONTEXT_ROOTS · EXECUTION_MAX_PROCESSES |
What a runner keeps on its side: run workspaces and clones, the CLI, the folders it may read, how many CLI processes at once. |
Everything else — queue lengths, timeouts, model prices, allowlists, the OTLP endpoint — is a setting, and the environment is still read as the middle layer of the three.