Model how your team really works.
Obversa lets you describe how a team works and run it with agents, code and people. One agent writes, another reviews, and a judge decides whether another revision is worth doing. The workflow carries the feedback back to the writer. A person makes the calls you leave to them.
Put that process in a TypeScript file. Use the agent CLIs you already have signed in, or give a role an API engine. Tests run as commands, reviews return their notes, and the run keeps its record in a plain file.
Use the same patterns to review a research brief, shape an article, answer a support ticket or deliver a feature. Notes go back to the writer. A check decides whether work continues. A recorded workflow can continue from its file after an interruption. No server, no database.
npm install @obversa/obversaNode.js 22.12 or later. @obversa/obversa installs the runtime and every
plugin but five engines, which you install on their own:
npm install @obversa/engine-jev-api for Jev,
npm install @obversa/engine-openai-decisions for OpenAI's Decisions API,
npm install @obversa/engine-devin-cli for Devin,
npm install @obversa/engine-mastra @mastra/core for a Mastra agent and
npm install @obversa/engine-openai-agents @openai/agents zod for an
OpenAI Agents SDK agent.
Use Claude Code, Codex, Grok or OpenCode with the command line tools you
already have signed in, or use an API engine.
Installation covers
setup. First run walks
through a complete file.
These are the parts of a process you recognise from working with people. Each snippet comes from a runnable example. Follow its link for the full file, inputs and result.
A writer drafts a post and a different model reads it against the house style. The writer gets the review notes and revises the draft. In the editorial example, Claude writes, Codex reviews, and a person is the editor:
roles: {
write: engines.claude('claude-sonnet-4-5'),
grade: [engines.codex('gpt-5.6-luna')],
editor: person('Publish this post?'),
},stage() from @obversa/runtime gives the writer a reviewer and a budget
for revisions:
stage('draft', {
agent: 'write',
writes: 'posts/draft.md',
desc: 'Write the post from the brief, in the house style.',
gate: 'The draft holds against every rule in style/house.md, as read by a grader from another model family.',
reviewedBy: 'grade',
// The judge. After a round the grader did not pass, Jev reads the
// findings and the rounds so far and says whether another round is
// worth it, for a finding tagged block too. The
// rounds end when Jev stops them or the grader passes the draft.
refine: judge(judgeSeat),
}),In workflow(), the reviewer has to come from a different model family
from the writer. Get a second opinion
explains the review loop.
A review can suggest changes that make little difference to the result.
Give a decision model the job of weighing those notes against the
purpose of the work and the revisions so far. judge() from
@obversa/runtime sets that stopping rule inside a dag() workflow:
judge: judge(judgeSeat),Here Jev makes the judgement, and the returns to the builder end when Jev
stops them or the review passes. Jev answers one question per finding, a
score from cosmetic to blocking, and two about the round:
product_decision, a noul asking whether a person must settle a
product decision first, and stop_reason, a choice of how the run
should end, none_applies included. Jev decides a blocking finding too.
Each answer is gated on the
confidence it reports: a finding goes back only at or over the send-back
bar for its level, a stop that would end the run needs the stop bar, and
a none_applies or an answer under the hand-off bar goes to a person. To
bound the returns as well, pass
judge(judgeSeat, { cap: 4 }): it allows four returns to the writer, and
Jev reads the review of the last one too. A product_decision Jev asks
for at the cap asks a person, and a change they ask for buys exactly one
more round.
Findings Jev still calls impactful change the ending: a count falling
from the round before grants one more round, and a count that is not
falling pauses the run to ask a person. The person can name "act" or
"hold" for each finding. A finding they hold joins minorFindings. A
finding Jev acted on that they do not name goes back with their answer,
unless they approve the work. When nothing goes back, the work stands as
a pass with no extra build. A run never ends as a silent fail while
findings Jev called impactful are still open. A finding Jev holds back, scored cosmetic or
minor or under its confidence bar, does not cost a round, whatever the
reviewer tagged it: Jev collects it on minorFindings, with its reviewer,
its level and its confidence, for a person to read before the work lands.
Know when to stop
shows the questions and a complete writing example.
Give the work to several reviewers and decide how many must agree. Each
opinion stays in the result, including dissent. A workflow() review
stage names the panel and the number of approvals it needs:
stage('review', {
panel: 'review',
agree: 1,
desc: 'Have both reviewers read the change and count the acceptances.',
gate: 'At least one reviewer has accepted.',
sendsBackTo: 'implement',
}),This example needs one approval from its two reviewers. Set agree to
two when both must accept. Ask a panel
shows the full team and what happens when a reviewer cannot answer.
A model's review can be enough to request another draft. Publishing may
need an editor's decision. The editorial workflow asks the person named
in its editor role after the model review passes:
stage('publish', {
input: 'editor',
desc: 'Put the graded draft in front of the editor.',
gate: 'The editor has said publish.',
sendsBackTo: 'draft',
}),With no answer the run pauses. A refusal with notes goes to the writer. Approval completes this example; a separate action would publish the post. Ask a person shows how the question and answer become part of the run.
Let a script check the facts while a model writes the report. The weekly report example uses a command to check that the draft includes every incident. Its exit code decides whether the writer tries again:
stage('check', {
run: [process.execPath, 'tools/check-report.mjs'],
desc: 'Fail the draft if an incident from the facts is missing.',
gate: 'The check exits 0.',
sendsBackTo: 'draft',
}),The workflow runs the command directly. The model spends its turn on the draft. Automate the routine work shows the same pattern with a test command.
A weekly report stops after gathering its facts. The next worker opens
the saved record and continues with the draft, without gathering those
facts again. run() from @obversa/runtime resumes the same workflow:
const second = await run(createReport(), {
recordTo: record,
resume: true,
onEvent: watch('worker-2', worker2),
});For steps declared with workflow() or dag(), the recovery rule is:
Steps that finished are never repeated. A step that was mid-flight when the worker died runs again only if its binding declares it safe to retry; otherwise the run pauses and asks a person to reconcile it before it continues, so uncertain work is never repeated silently. The complete example shows which stages each worker ran.
The pattern collection also covers comparing different approaches, bringing a team together, and preparing context before handing over a task. Patterns can be combined inside a larger workflow.
A real use case combines these patterns around an outcome:
| Workflow | How the team works |
|---|---|
| Review an article | A writer drafts, a different model reviews, and an editor decides whether it is ready to publish. |
| Read research papers | A model summarises supplied papers, a person selects the notes to keep, and those notes inform an answer. |
| Handle support tickets | Classify each ticket, draft a reply, and refer uncertain or sensitive cases to a person. |
| Prepare a shortlist | Apply rules in code, compare rankings from two models, and ask a person to choose the shortlist. |
| Deliver a feature | Build and test the change, check every requirement, review it with three model families and approve the exact bytes. |
Browse the workflows for the full files and examples from other fields.
The feature team takes a ticket through seven steps:
- Build. One seat builds the change from the ticket.
- Checks. The ticket's tests run. A red test goes back to the builder with its output.
- Goal check. A seat from another model family confirms that every requirement in the ticket was met.
- Review battery. Three reviewers from three model families review the change at the same time.
- Synthesis. Their reviews become one list.
- Judge. Jev scores each finding cosmetic, minor, impactful or blocking, each with a confidence it reports. An impactful or blocking finding at or over its confidence bar goes back to the builder; every other finding is held back for a person to read.
- Approval. Attended, a person approves the exact bytes of every
file the change adds, changes or deletes. With
--unattended, the change lands without asking.
nodes: {
build: {
desc: `Make the change in ${files.join(', ')}. Do not change ${tests.join(', ')}.`,
gate: 'The ticket is met and its tests pass.',
agent: builder,
writes: files,
// The node's own refinement, each round: the ticket's tests first,
// a red one goes straight back with its output; then the goal
// check; then the three reviewers as one panel; then Jev scores
// each kept finding cosmetic to blocking, a block included, with
// no cap; a held-back one is collected for a person to read.
refine: review({
checks: [['node', '--test', ...tests]],
goal: goalCheck(goalSeat, { target: 'build', text: ticket.brief }),
reviewers: [
{ name: 'codex', seat: reviewers.codex },
{ name: 'gemini', seat: reviewers.gemini },
{ name: 'grok', seat: reviewers.grok },
],
// The merge asks for no tools, a call Codex refuses; Grok takes it.
synthesise: reviewers.grok,
judge: judge(judgeSeat),
}),
},
...(attended ? { approve: { needs: 'build', job: approve(start!) } } : {}),
},Each ticket runs in its own worktree, and the change lands on your branch
when the run passes. examples/teams/feature-team-backlog.ts points the
same team at a folder of tickets and delivers them one at a time. The
feature delivery file is examples/teams/feature-delivery.ts.
A seat names the tool, the provider, the model family and the model.
| package | drives | needs |
|---|---|---|
@obversa/engine-claude-cli |
Claude Code, one fresh process per attempt | Claude Code, signed in |
@obversa/engine-codex-cli |
Codex | Codex, signed in |
@obversa/engine-devin-cli |
Devin | Devin, signed in |
@obversa/engine-grok-cli |
Grok | the Grok command line tool |
@obversa/engine-opencode-cli |
OpenCode | the OpenCode command line tool |
@obversa/engine-claude-agent-sdk |
the Claude Agent SDK | Claude auth |
@obversa/engine-anthropic-api |
the Anthropic API | an API key |
@obversa/engine-jev-api |
Jev, typed decisions over recorded state | a Jev endpoint and API key |
@obversa/engine-mastra |
an agent you built with Mastra | @mastra/core and what your agent needs |
@obversa/engine-openai-agents |
an agent you built with the OpenAI Agents SDK | @openai/agents, zod and what your agent needs |
Write your own against the engine contract in @obversa/api; it must pass
the conformance kit.
Keep a workflow in a central collection and point each run at a repository
with run(job, { cwd }), keep it beside the code it works on, or import it
from a service and call run() when the service decides. It runs the same
way from each.
Where a workflow lives.
obversa.ai/docs: the first run, the concepts, the patterns, examples by field, and how Obversa sits beside LangGraph, CrewAI, Temporal, Claude Code subagents and eve.
24 publishable packages. packages/ holds the nine that define the
product: @obversa/runtime runs a workflow, @obversa/api holds the
engine and memory contracts, @obversa/core runs bounded child processes,
@obversa/runner supervises stored runs, @obversa/rdc ranks, decides
and curates memory candidates, @obversa/builtin-workflows
ships three ready-made teams and a workflow that proposes changes to a
workflow from its record, @obversa/obversa is the install above,
and @obversa/surface with @obversa/surface-diff is the local review
page. plugins/ holds the fifteen adapters: the eleven engines above, three
memories, and a notifier that posts run events to a URL.
AGENTS.md is the guide for anyone who changes this repository: setup, the checks, and the rules every change follows.
Obversa uses the MIT License. Report security problems as described in the security policy.