Agent SDK Reference
Every phase in a cliq pipeline executes through an agent — a lightweight process that reads context from .cliq/, does work (via a CLI tool, an LLM API, or a human review server), writes results back, and exits. The agent handles all protocol mechanics; the implementation does the creative work.
The implementation never touches
.cliq/— only the agent wrapper does.
For most users, the built-in agents are all you need. If you want a phase powered by a different LLM or custom logic, the @cliqhub/cliq-sdk package lets you build your own agent in under 50 lines.
Built-in Agents
Section titled “Built-in Agents”Cliq ships 22+ built-in agents — CLI agents, LLM API agents, connector agents, and special-purpose agents. See the Agent Reference for the full catalog with per-agent documentation, setup guides, and handoff structures.
CLI agents (cursor, claude-code, gemini, codex) require their respective binary installed. All other agents only need settings configured via cliq settings. Run cliq doctor to check availability.
Choosing an Agent
Section titled “Choosing an Agent”Global default
Section titled “Global default”Set the default agent for all projects in ~/.cliqrc/settings.json:
{ "default_agent": "claude-code"}If omitted, the default is cursor.
Per-phase override
Section titled “Per-phase override”Any phase in team.yml can specify a different agent:
workflow: phases: - name: research type: standard agent: gemini
- name: implementation type: standard # uses the default (cursor or whatever default_agent is set to)CLI flag
Section titled “CLI flag”Override the default for a single run:
cliq run --agent claude-codeThis sets default_agent to claude-code for this invocation only. Phases with an explicit agent in team.yml are not affected.
Quick reference
Section titled “Quick reference”| Method | Scope | Where | Example |
|---|---|---|---|
| Global default | All projects, all phases | ~/.cliqrc/settings.json | "default_agent": "gemini" |
| Per-phase | Single phase in one team | team.yml | phases: [{ name: research, agent: gemini }] |
| CLI flag | Single run, all default phases | Command line | cliq run --agent claude-code |
Custom Agents
Section titled “Custom Agents”Teams can bundle custom agent implementations — for example, an OpenAI-powered analyst or a specialized gate. Custom agents are declared in the agents section of team.yml:
agents: openai-analyst: entry: agents/analyst/index.js| Field | Type | Required | Description |
|---|---|---|---|
entry | string | yes | Path to the agent entry point, relative to the team directory |
env | string[] | no | Environment variable names the agent requires from the host OS |
All 22 built-in agents are available by name without declarations.
Users configure agent settings (API keys, credentials) in ~/.cliqrc/settings.json under agents.<name>:
{ "agents": { "openai-analyst": { "api_key": "sk-..." } }}See Settings — agents for the full reference.
Install the SDK
Section titled “Install the SDK”npm install @cliqhub/cliq-sdkRequires Node.js 18+.
If you’re adding an agent to a team package:
mkdir -p agents/my-agentcd agents/my-agentnpm init -ynpm install @cliqhub/cliq-sdkWhen to build a custom agent
Section titled “When to build a custom agent”Use the SDK when:
- You want a phase powered by a different LLM API (OpenAI, Anthropic, Google, Mistral, etc.)
- You need custom logic beyond what a CLI agent provides (multi-step reasoning, tool orchestration, database access, external API calls)
- You want to wrap an existing service or internal tool as a pipeline phase
- You need a custom gate with domain-specific review logic
For simply switching between supported CLI agents (Cursor, Claude Code, Gemini, Codex), use the agent field in team.yml — no SDK needed.
Quick Start — Standard Phase Agent
Section titled “Quick Start — Standard Phase Agent”A standard phase agent does work and produces output for downstream phases.
import { CliqAgent, build_phase_context, finalize_standard_phase } from '@cliqhub/cliq-sdk';
new CliqAgent({ name: 'openai-analyst',
handlers: { 'phase:standard': async (msg) => { const ctx = build_phase_context(msg, 'openai-analyst'); ctx.progress('Starting analysis...');
const response = await call_your_llm(ctx.role, ctx.input_text);
for (const target of ctx.targets) { ctx.send(target, response.text); }
finalize_standard_phase(ctx, msg); }, },}).run();Declare it in team.yml:
name: "@local/research-team"
agents: openai-analyst: entry: agents/analyst/index.ts
workflow: phases: - name: research type: standard
- name: analysis type: standard agent: openai-analyst depends_on: [research]
- name: review type: gate depends_on: [analysis]Run:
cliq assemble research-teamcliq runThe orchestrator builds an AgentMessage JSON envelope, pipes it to your agent via stdin, and the SDK handles everything else — parsing the message, assembling context, calling your handler, writing channels, and signaling completion via the JSONL signal channel.
Quick Start — Gate Phase Agent
Section titled “Quick Start — Gate Phase Agent”A gate agent reviews work and renders a verdict: pass, route back, or escalate.
import { CliqAgent, GateContext, build_phase_context, finalize_gate_phase } from '@cliqhub/cliq-sdk';
new CliqAgent({ name: 'strict-reviewer',
handlers: { 'phase:gate': async (msg) => { const ctx = build_phase_context(msg, 'strict-reviewer') as GateContext;
const all_pass = ctx.check_results.every(r => r.pass);
if (!all_pass) { const failed = ctx.check_results .filter(r => !r.pass) .map(r => r.name);
const result = { verdict: { outcome: 'ROUTE', target: ctx.route_targets[0] }, comment: `Checks failed: ${failed.join(', ')}`, }; finalize_gate_phase(ctx, result, msg); return; }
const review = await call_llm_for_review(ctx.role, ctx.inputs);
if (review.approved) { const result = { verdict: { outcome: 'PASS' }, comment: review.reason, }; finalize_gate_phase(ctx, result, msg); return; }
const result = { verdict: { outcome: 'ROUTE', target: ctx.route_targets[0] }, comment: review.reason, }; finalize_gate_phase(ctx, result, msg); }, },}).run();Gate verdicts are type-safe discriminated unions:
| Verdict | Shape | Meaning |
|---|---|---|
| Pass | { outcome: 'PASS' } | All good — pipeline continues forward |
| Route | { outcome: 'ROUTE', target: '<phase>' } | Send back to a specific phase for rework |
| Escalate | { outcome: 'ESCALATE' } | Requires human intervention — pipeline pauses |
The SDK validates ROUTE targets at runtime. If the target is not in ctx.route_targets, the agent fails with a clear error.
The Handler Function
Section titled “The Handler Function”The handler function is the core of your agent. Each handler receives an AgentMessage and uses build_phase_context(msg, name) to get a typed context object. The context object fields remain the same — build_phase_context assembles the context from the message’s project_dir, reading .cliq/ protocol files, and returns a PhaseContext or GateContext depending on the phase type. After your handler completes its work, call finalize_standard_phase(ctx, msg) or finalize_gate_phase(ctx, result, msg) to write channels and signal completion.
AgentMessage
Section titled “AgentMessage”The AgentMessage is the JSON envelope piped to every agent invocation via stdin. It contains everything the agent needs — identity, paths, settings, and a type-specific payload.
import { AgentMessage } from '@cliqhub/cliq-sdk';| Field | Type | Description |
|---|---|---|
version | number | Protocol version (currently 1) |
type | string | Message type — determines routing. Values: "phase", "notify", "test". For "phase", the handler key is derived from payload.phase_type (e.g. "phase:standard" or "phase:gate") |
agent | string | Agent name (e.g. "openai-analyst") |
instance_id | string | Pipeline run instance ID |
team_name | string | Team package name |
req_key | string | Requirement key (e.g. Jira ticket) |
project_dir | string | Absolute path to the project root |
work_dir | string | Project workspace root — where agents operate on files |
signal_dir | string | Directory where the JSONL signal channel (<phase>.jsonl) is written |
handoff_dirs | string[] | Absolute paths to output channel directories — one per downstream phase in the DAG |
input_artifacts | Artifact[] | Input artifacts — specs, upstream handoffs, inline content. Each artifact has name, type ("file" / "uri" / "inline"), and source (a path, URL, or literal content) |
settings | Record<string, unknown> | Agent-specific settings from settings.json, filtered to declared keys |
payload | Record<string, unknown> | Type-specific payload — phase config for phase:*, notification data for notify, empty for test |
Signal Channel
Section titled “Signal Channel”Every invocation has a signal_dir where the SDK appends messages to a JSONL signal channel (<phase>.jsonl). All messages conform to the ChannelMessage interface:
import { ChannelMessage } from '@cliqhub/cliq-sdk';| Field | Type | Description |
|---|---|---|
ts | string | ISO timestamp |
version | 1 | Protocol version |
type | string | Dispatch type: "phase", "notify", or "test" |
status | string | "ok", "complete", "error", "progress", or "notify" |
result | unknown? | Handler return value. Present when status is "complete". For gates, contains { verdict: { outcome, target?, reason } } |
error | string? | Error message when status is "error" |
message | string? | Human-readable progress note when status is "progress" |
The SDK writes these automatically — you don’t write channel messages yourself. The signal lifecycle is:
- Ack —
{status: "ok"}appended after parsing the message. The orchestrator uses this to confirm the agent started. - Progress (optional) —
{status: "progress", message: "..."}appended during execution viactx.progress(). - Notify (optional) —
{status: "notify", result: {...}}appended when the agent registers a notification dispatch. - Completion —
{status: "complete", result: {...}}or{status: "error", error: "..."}appended when the handler finishes. For gates, theresultfield carries the verdict.
The channel abstraction uses FileChannelWriter (JSONL append) in the SDK and FileChannelReader in the orchestrator, both implementing transport-agnostic interfaces for future migration to localhost HTTP channels.
PhaseContext (standard phases)
Section titled “PhaseContext (standard phases)”| Field | Type | Description |
|---|---|---|
phase | string | Phase name (e.g. "analysis") |
role | string | Full role brief from .cliq/roles/<phase>.md |
is_support | boolean? | Whether this phase is a support phase activated by gate routing |
input_envelopes | Record<string, HandoffEnvelope> | Raw handoff envelopes from predecessor phases, keyed by channel name (e.g. "architect--developer"). Each envelope has version, phase_type, phase, agent, data?, and text? fields. Use this when you need structured access to upstream data. |
input_text | Record<string, string> | Pre-rendered, prompt-ready text from predecessor handoffs, keyed by channel name. The SDK automatically renders envelopes into a format suitable for LLM consumption — including both structured data (as formatted JSON) and natural language text. For most agents, this is the only input field you need. |
dispatch | object | Agent-specific configuration from the phase YAML. Contains action, sources, target_entries, commands, model, and review — only the fields relevant to your agent type are populated. See Dispatch Fields below. |
req_spec | string | Requirements specification from .cliq/req_spec.md |
task_board | TaskBoard | Structured task board interface (see TaskBoard API below) |
design_docs | Record<string, string> | Design documents, keyed by filename |
workflow_inputs | Record<string, string>? | Resolved team inputs from .cliq/inputs.json, available to all phases |
targets | string[] | Downstream phase names this phase can send handoff notes to |
send(target, payload) | function | Send a handoff to a downstream phase. Accepts a string or { data?, text? } object. Writes handoff.json to disk immediately |
write_file(name, content) | function | Write a file to .cliq/files/{phase}/{name}. Used by connector agents to persist fetched data or generated output to the workspace |
settings | Record<string, unknown> | Agent-specific settings from settings.json, resolved from the agent’s manifest settings.required / settings.optional declarations. Contains API keys, service URLs, credentials, and any other configuration the agent declared |
project_dir | string | Absolute path to the project root |
work_dir | string | Project workspace root — where agents operate on files |
state_dir | string | Orchestration state directory (call frame) — where signals/channels live |
metadata.instance_id | string | Pipeline run instance ID |
metadata.team_name | string | Team package name |
metadata.req_key | string | Requirement key (e.g. Jira ticket) |
progress(msg) | function | Log a status message (visible in the tmux pane) |
signal(name, payload) | function | Write a named signal ({phase}_{name}.json) to .cliq/signals/ |
register_notification(event, channel_ref, payload) | function | Register a notification for inline dispatch via the JSONL channel. event is the trigger name (e.g. hug_review), channel_ref is the delivery target in agent:channel format (e.g. slack:oncall, email:ops), and payload is a free-form object. Writes a {status: "notify"} message to the signal channel; the orchestrator dispatches it to the named agent |
Dispatch Fields
Section titled “Dispatch Fields”The ctx.dispatch object carries phase YAML configuration that varies by agent type. Phase-specific fields live under ctx.dispatch.phase_config:
const action = ctx.dispatch.phase_config.action as string; // connector action stringconst sources = ctx.dispatch.phase_config.sources as SourceEntry[]; // SourceEntry[]const model = ctx.dispatch.phase_config.model as string; // LLM model override| Field | Type | Description |
|---|---|---|
action | string? | Connector action (e.g. get_issue, search). Set from phase YAML |
sources | SourceEntry[]? | Declared data sources to fetch (see SourceEntry) |
target_entries | TargetEntry[]? | Declared data targets to deliver (see TargetEntry) |
commands | Command[]? | Shell commands for exec agents (see Command) |
model | string? | LLM model override from phase YAML |
review | ReviewBlock? | Review configuration for hug gates: reviewer, artifacts, timeout, remind_every |
GateContext (gate phases)
Section titled “GateContext (gate phases)”GateContext extends PhaseContext — gate agents have access to all the same fields (input_envelopes, input_text, dispatch, req_spec, task_board, etc.) plus these gate-specific additions:
| Field | Type | Description |
|---|---|---|
check_results | GateCheckResult[] | Results of automated checks (name, pass, output, exit_code) |
route_targets | string[] | Valid phases this gate can route work back to |
iteration | number | Current gate iteration (1-based) |
max_iterations | number | Maximum gate loop iterations before auto-escalation |
Standard phase return value
Section titled “Standard phase return value”Standard phase agents return void. All output is written imperatively during execution:
- Channels —
ctx.send(target, payload)writes ahandoff.jsonenvelope to disk immediately - Task board —
ctx.task_board.mark_complete(),.add_note(), etc.
This means partially completed work survives agent crashes — whatever was sent before the failure is preserved on disk.
ctx.send() accepts either a string (shorthand for { text: string }) or a structured payload:
// String shorthand — sets the text fieldctx.send('reviewer', 'Implementation complete. All tests pass.');
// Structured payload — sets both data and textctx.send('reviewer', { data: { files_changed: 3, test_count: 12, coverage: 94.2 }, text: 'Implementation complete. 12 tests pass with 94.2% coverage.',});The data field is accessible downstream via $(handoff.<phase>.<path>) template variables. See Template Variables for details.
GateResult (gate phases)
Section titled “GateResult (gate phases)”| Field | Type | Required | Description |
|---|---|---|---|
verdict | VerdictOutcome | yes | { outcome: 'PASS' }, { outcome: 'ROUTE', target: '...' }, or { outcome: 'ESCALATE' } |
comment | string | yes | Explanation for the verdict |
reviewer | string | no | Who made the decision |
decided_at | string | no | ISO timestamp of the decision |
Complete Examples
Section titled “Complete Examples”OpenAI single-shot agent
Section titled “OpenAI single-shot agent”import { CliqAgent, build_phase_context, finalize_standard_phase } from '@cliqhub/cliq-sdk';import OpenAI from 'openai';
const client = new OpenAI();
new CliqAgent({ name: 'openai-researcher',
handlers: { 'phase:standard': async (msg) => { const ctx = build_phase_context(msg, 'openai-researcher');
const completion = await client.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: ctx.role }, { role: 'user', content: ctx.input_text || ctx.req_spec }, ], });
const content = completion.choices[0].message.content ?? '';
for (const target of ctx.targets) { ctx.send(target, content); }
finalize_standard_phase(ctx, msg); }, },}).run();Multi-turn agent with tool use
Section titled “Multi-turn agent with tool use”import { CliqAgent, build_phase_context, finalize_standard_phase } from '@cliqhub/cliq-sdk';
new CliqAgent({ name: 'tool-agent',
handlers: { 'phase:standard': async (msg) => { const ctx = build_phase_context(msg, 'tool-agent'); let context = build_initial_context(ctx.role, ctx.input_text); let iterations = 0;
while (iterations < 10) { ctx.progress(`Iteration ${iterations + 1}...`);
const response = await call_llm(context);
if (response.tool_calls) { const results = await execute_tools(response.tool_calls); context = append_tool_results(context, results); iterations++; continue; }
for (const target of ctx.targets) { ctx.send(target, response.text); }
finalize_standard_phase(ctx, msg); return; } }, },}).run();Anthropic gate agent
Section titled “Anthropic gate agent”import { CliqAgent, GateContext, build_phase_context, finalize_gate_phase } from '@cliqhub/cliq-sdk';import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
new CliqAgent({ name: 'claude-reviewer',
handlers: { 'phase:gate': async (msg) => { const ctx = build_phase_context(msg, 'claude-reviewer') as GateContext;
const failed_checks = ctx.check_results.filter(r => !r.pass); if (failed_checks.length > 0) { const result = { verdict: { outcome: 'ROUTE', target: ctx.route_targets[0] }, comment: `${failed_checks.length} check(s) failed: ${failed_checks.map(c => c.name).join(', ')}`, }; finalize_gate_phase(ctx, result, msg); return; }
const llm_msg = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 2048, system: ctx.role, messages: [{ role: 'user', content: `Review the following work:\n\n${ctx.input_text}\n\nRespond with PASS if the work meets all requirements, or ROUTE if it needs changes. Explain your reasoning.`, }], });
const text = llm_msg.content[0].type === 'text' ? llm_msg.content[0].text : ''; const is_pass = text.toUpperCase().includes('PASS');
if (is_pass) { const result = { verdict: { outcome: 'PASS' }, comment: text, decided_at: new Date().toISOString(), }; finalize_gate_phase(ctx, result, msg); return; }
const result = { verdict: { outcome: 'ROUTE', target: ctx.route_targets[0] }, comment: text, decided_at: new Date().toISOString(), }; finalize_gate_phase(ctx, result, msg); }, },}).run();Agent that updates the task board
Section titled “Agent that updates the task board”The ctx.task_board object exposes structured methods — agents never
manipulate the markdown format directly.
import { CliqAgent, build_phase_context, finalize_standard_phase } from '@cliqhub/cliq-sdk';
new CliqAgent({ name: 'task-tracker',
handlers: { 'phase:standard': async (msg) => { const ctx = build_phase_context(msg, 'task-tracker'); ctx.progress('Working on tasks...');
const result = await do_work(ctx);
ctx.task_board.mark_complete(ctx.phase); ctx.task_board.add_note(ctx.phase, `Completed: ${result.summary}`);
for (const target of ctx.targets) { ctx.send(target, result.text); }
finalize_standard_phase(ctx, msg); }, },}).run();See TaskBoard API for the full interface.
Agent Settings
Section titled “Agent Settings”How settings reach your agent
Section titled “How settings reach your agent”Agents receive their configuration through ctx.settings — a Record<string, unknown> containing only the keys declared in the agent’s manifest. The orchestrator reads settings from settings.json (agents.<name>.*), filters them to the declared keys, and passes them to the agent at dispatch time.
handlers: { 'phase:standard': async (msg) => { const ctx = build_phase_context(msg, 'my-agent'); const api_key = ctx.settings.api_key as string; // Use the key to call an API, set process.env, etc. },},Built-in agents
Section titled “Built-in agents”Built-in agents declare their required settings in their manifest.json under settings.required. You don’t need to list them in team.yml — cliq doctor and cliq assemble read manifests automatically.
Set per-agent settings in settings.json under agents.<name>:
{ "agents": { "cursor": { "api_key": "key_abc123..." }, "claude-code": { "api_key": "sk-ant-..." } }}Use cliq setup to configure these interactively, or set them with cliq settings:
cliq settings agents.cursor.api_key key_abc123...Custom agents
Section titled “Custom agents”Custom agents declare required settings in their manifest.json under settings.required. They are registered in team.yml under agents: and resolved via agents.json:
{ "name": "openai-analyst", "version": "1.0.0", "entry": "./index.js", "capabilities": ["phase:standard"], "settings": { "required": ["api_key"] }}Set the values in settings the same way:
{ "agents": { "openai-analyst": { "api_key": "sk-..." } }}Host environment variables
Section titled “Host environment variables”If your agent wraps a CLI binary that expects specific environment variables, declare them in the manifest’s env array. These are host OS environment variables the agent expects to inherit — separate from cliq-managed settings:
{ "env": ["PATH", "HOME"]}The agent is responsible for mapping ctx.settings values to process.env if its internal library requires it.
Testing Your Agent
Section titled “Testing Your Agent”Unit testing with vitest
Section titled “Unit testing with vitest”The CliqAgent class is designed for testability. Use agent.dispatch(msg) in tests instead of run() (which reads stdin and calls process.exit). Build an AgentMessage with the make_message() helper or construct one manually.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';import fs from 'node:fs';import path from 'node:path';import os from 'node:os';import { CliqAgent, AgentMessage, build_phase_context, finalize_standard_phase,} from '@cliqhub/cliq-sdk';
describe('my analyst agent', () => { let project_dir: string; let signal_dir: string;
beforeEach(() => { project_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-')); signal_dir = path.join(project_dir, '.cliq', 'signals', 'analyst');
const cliq = (...p: string[]) => path.join(project_dir, '.cliq', ...p); fs.mkdirSync(signal_dir, { recursive: true }); fs.mkdirSync(cliq('roles'), { recursive: true }); fs.mkdirSync(cliq('channels'), { recursive: true });
fs.writeFileSync(cliq('roles', 'analyst.md'), 'You are an analyst.'); });
afterEach(() => { fs.rmSync(project_dir, { recursive: true, force: true }); });
it('produces output on the expected channel', async () => { const msg: AgentMessage = { version: 1, type: 'phase', agent: 'openai-analyst', instance_id: 'test-1', team_name: 'test', req_key: 'TEST-1', project_dir, work_dir: project_dir, signal_dir, handoff_dirs: [path.join(project_dir, '.cliq', 'channels', 'analyst--reviewer')], input_artifacts: [], settings: {}, payload: { phase_type: 'standard', phase: 'analyst' }, };
const agent = new CliqAgent({ name: 'openai-analyst', handlers: { 'phase:standard': async (m) => { const ctx = build_phase_context(m, 'openai-analyst'); for (const target of ctx.targets) { ctx.send(target, 'Analysis results here.'); } finalize_standard_phase(ctx, m); }, }, });
await agent.dispatch(msg);
const cliq = (...p: string[]) => path.join(project_dir, '.cliq', ...p); const envelope = JSON.parse(fs.readFileSync( cliq('channels', 'analyst--reviewer', 'handoff.json'), 'utf8', )); expect(envelope.text).toBe('Analysis results here.'); expect(envelope.phase_type).toBe('standard');
const channel_path = path.join(signal_dir, 'analyst.jsonl'); const lines = fs.readFileSync(channel_path, 'utf8').trim().split('\n'); const last = JSON.parse(lines[lines.length - 1]); expect(last.status).toBe('complete'); });});Manual testing
Section titled “Manual testing”Test your agent by piping an AgentMessage JSON to stdin:
# 1. Set up minimal protocol filesmkdir -p .cliq/signals .cliq/roles .cliq/channels/researcher--analyst .cliq/channels/analyst--reviewer .cliq/designecho "You are a research analyst." > .cliq/roles/analyst.mdecho '{"version":1,"phase_type":"standard","phase":"researcher","agent":"exec","text":"Research findings."}' > .cliq/channels/researcher--analyst/handoff.jsonecho "Analyze the data." > .cliq/req_spec.md
# 2. Build an AgentMessage and pipe it to the agentcat << 'EOF' | node agents/analyst/index.js{ "version": 1, "type": "phase", "agent": "openai-analyst", "instance_id": "test-1", "team_name": "test", "req_key": "TEST-1", "project_dir": "/path/to/your/project", "work_dir": "/path/to/your/project", "signal_dir": "/path/to/your/project/.cliq/signals", "handoff_dirs": ["/path/to/your/project/.cliq/channels/analyst--reviewer"], "input_artifacts": [], "settings": {}, "payload": { "phase_type": "standard", "phase": "analyst" }}EOF
# 3. Verify resultscat .cliq/channels/analyst--reviewer/handoff.jsoncat .cliq/signals/analyst.jsonlAPI Reference
Section titled “API Reference”CliqAgent
Section titled “CliqAgent”import { CliqAgent, AgentMessage } from '@cliqhub/cliq-sdk';
new CliqAgent({ name: string, handlers: { 'phase:standard'?: (msg: AgentMessage) => Promise<unknown>, 'phase:gate'?: (msg: AgentMessage) => Promise<unknown>, 'notify'?: (msg: AgentMessage) => Promise<unknown>, 'test'?: (msg: AgentMessage) => Promise<unknown>, },}).run();| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Agent name (e.g. "openai-analyst") |
handlers | object | yes | Map of handler functions keyed by message type. Provide one or more handler keys |
handlers['phase:standard'] | function | no | Standard phase handler — receives an AgentMessage, uses build_phase_context(msg, name) to get a PhaseContext. Call finalize_standard_phase(ctx, msg) when done |
handlers['phase:gate'] | function | no | Gate phase handler — receives an AgentMessage, uses build_phase_context(msg, name) to get a GateContext. Call finalize_gate_phase(ctx, result, msg) with the verdict to write the signal channel and the handoff envelope |
handlers['notify'] | function | no | Notification handler — receives an AgentMessage with notification payload. Invoked when the orchestrator dispatches the agent to deliver a pipeline event notification (e.g. on_complete, on_escalate) |
handlers['test'] | function | no | Connectivity test — receives an AgentMessage and returns a TestResult. Invoked by cliq doctor agent <name> to verify the agent can reach its external dependencies. A default handler is provided if not specified |
An agent can implement one or more handlers. The orchestrator dispatches to the correct handler based on the AgentMessage.type field.
Call .run() to start the CLI lifecycle (reads AgentMessage JSON from stdin, dispatches, exits). Use .dispatch(msg) in tests.
Agent requirements (environment variables, binary dependencies, etc.) are declared in the accompanying manifest.json — not in the constructor. See manifest.json below.
VerdictOutcome
Section titled “VerdictOutcome”type VerdictOutcome = | { outcome: 'PASS' } | { outcome: 'ROUTE'; target: string } | { outcome: 'ESCALATE' };Type-safe discriminated union for gate verdicts. The SDK validates ROUTE targets against ctx.route_targets at runtime.
manifest.json
Section titled “manifest.json”Every agent ships a manifest.json that declares its requirements, capabilities, and metadata. This is the file cliq doctor, cliq assemble, and cliq setup read to validate that all agent prerequisites are met before a run starts.
{ "name": "my-agent", "version": "1.0.0", "description": "What the agent does", "entry": "./agent.js", "capabilities": ["phase:standard", "phase:gate"], "role": "required", "binaries": ["git"], "settings": { "required": ["api_key"], "optional": ["model"] }, "env": ["PATH"], "schema": {}, "output": { "data": null, "text": "Description of output" }}| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Agent name — must match the name passed to CliqAgent |
version | string | yes | Semver version string |
min_cliq_version | string | no | Minimum cliq version required to run this agent |
description | string | yes | Human-readable description of what the agent does |
entry | string | yes | Path to the agent entry point, relative to the manifest directory |
capabilities | string[] | yes | Message types the agent supports: "phase:standard", "phase:gate", "notify", or any combination |
role | string | no | Agent role hint (e.g. "required", "optional") |
settings | object | no | Settings requirements — keys the agent reads from ctx.settings |
settings.required | string[] | no | Settings keys the agent requires (validated at assemble/doctor time) |
settings.optional | string[] | no | Settings keys the agent can use but doesn’t require |
binaries | string[] | no | System binaries the agent requires (e.g. ["git"]). cliq doctor checks each binary is on PATH |
env | string[] | no | Host environment variables the agent expects to inherit from the OS |
schema | object | yes | Schema for the agent’s phase YAML configuration — a Record<string, SchemaField> declaring each field’s type, required, and optional enum, default, description |
output | object | no | Description of the agent’s output shape |
output.data | any | no | Schema or example of structured data output |
output.text | string | no | Description of text output |
Place manifest.json alongside your agent entry point. Custom agents registered in team.yml under agents: are resolved via agents.json, which is generated by cliq assemble.
SourceEntry
Section titled “SourceEntry”Declared data source on a phase — used by connector agents to fetch external data.
interface SourceEntry { name: string; ref?: string; // system-specific reference (doc ID, S3 path, issue key, etc.) url?: string; // HTTP URL (for curl agent) headers?: Record<string, string>; method?: string; // HTTP method override (default: GET, or POST when body is present) body?: string; // request body (for curl agent). When present, method defaults to POST [key: string]: unknown; // additional connector-specific fields}TargetEntry
Section titled “TargetEntry”Declared data target on a phase — used by connector agents to deliver output to external systems.
interface TargetEntry { name: string; file: string; // local file to deliver ref?: string; // system-specific destination url?: string; // HTTP URL (for curl agent) headers?: Record<string, string>; method?: string; // HTTP method override (default: POST) mode?: 'create' | 'append' | 'replace';}Command
Section titled “Command”Shell command definition for exec agents.
interface Command { name: string; run: string; escalate_on_fail?: boolean;}GateCheckResult
Section titled “GateCheckResult”Result of a single automated check in a gate phase.
interface GateCheckResult { name: string; pass: boolean; output: string; exit_code?: number;}test — Agent Connectivity Tests
Section titled “test — Agent Connectivity Tests”Agents can implement a 'test' handler to let cliq doctor verify connectivity to external services. When a user runs cliq doctor agent <name>, the SDK dispatches an AgentMessage with type: 'test' and the agent’s settings. The handler returns a TestResult.
new CliqAgent({ name: 'my-api-agent',
handlers: { 'phase:standard': async (msg) => { /* ... */ },
'test': async (msg) => { const api_key = msg.settings.api_key as string; if (!api_key) { return { passed: false, message: 'Missing api_key in settings' }; }
const resp = await fetch('https://api.example.com/ping', { headers: { Authorization: `Bearer ${api_key}` }, });
if (!resp.ok) { return { passed: false, message: `API returned ${resp.status}` }; }
return { passed: true, message: 'Connected to example API' }; }, },}).run();If a 'test' handler is not provided, the SDK uses a default handler that returns a pass result.
The 'test' handler receives an AgentMessage with type: 'test'. Agent settings are available at msg.settings.
TestResult
Section titled “TestResult”| Field | Type | Description |
|---|---|---|
passed | boolean | Whether the connectivity test passed |
message | string | Human-readable description of the result |
Notify Handler
Section titled “Notify Handler”The 'notify' handler receives an AgentMessage with type: 'notify'. The notification payload is available via msg.payload, which contains event, channel, and event-specific data. This happens when a pipeline event (configured in notifications.* settings) triggers a notification delivery.
The msg.payload for notify messages conforms to NotifyPayload:
| Field | Type | Description |
|---|---|---|
notifications | Notification[] | Batch of notification events targeting this agent |
Each Notification in the array has:
| Field | Type | Description |
|---|---|---|
event | string | Pipeline event name (e.g. on_complete, on_escalate, hug_review) |
channel | string | null | Channel within the agent (e.g. "alerts", "ops"), or null for the default channel |
phase_name | string? | Phase that triggered the event |
reason | string? | Human-readable reason for the event |
outcome | string? | Pipeline outcome at event time |
[key] | unknown | Additional event-specific data (varies by event type) |
import { CliqAgent, AgentMessage, NotifyPayload } from '@cliqhub/cliq-sdk';
new CliqAgent({ name: 'slack',
handlers: { 'phase:standard': async (msg) => { /* ... */ },
'notify': async (msg) => { const payload = msg.payload as NotifyPayload; const channels = msg.settings.channels as Record<string, { webhook_url: string }>;
for (const notification of payload.notifications) { const channel_key = notification.channel ?? 'default'; const webhook_url = channels[channel_key]?.webhook_url; if (!webhook_url) continue;
await fetch(webhook_url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `Pipeline event: ${notification.event}`, blocks: format_blocks(notification), }), }); } }, },}).run();Agents that implement the 'notify' handler (like slack and email) are automatically eligible for notification dispatch. The orchestrator matches the channel prefix (e.g. email:ops resolves to the email agent with channel ops; bare slack resolves to the slack agent with channel default).
TaskBoard API
Section titled “TaskBoard API”The ctx.task_board object provides structured methods for updating the task board.
Agents never manipulate the internal markdown format directly — the SDK handles
serialization and only writes .cliq/task_board.md when mutations have been made.
| Method / Property | Description |
|---|---|
mark_complete(phase) | Toggle - [ ] phase → - [x] phase. No-op if already complete or not found. |
mark_incomplete(phase) | Toggle - [x] phase → - [ ] phase. No-op if already incomplete or not found. |
add_note(phase, note) | Insert a note line below the matching phase entry. No-op if phase not found. |
replace(content) | Replace the entire board with new content (for LLM-generated rewrites). |
content | Read-only string — current board content (pass to an LLM as context). |
dirty | Read-only boolean — true if any mutation has been made. |
handlers: { 'phase:standard': async (msg) => { const ctx = build_phase_context(msg, 'my-agent');
ctx.task_board.mark_complete(ctx.phase); ctx.task_board.add_note(ctx.phase, 'OAuth2 PKCE flow implemented');
for (const target of ctx.targets) { ctx.send(target, 'OAuth2 PKCE flow implemented.'); }
finalize_standard_phase(ctx, msg); },},The SDK writes the task board to disk automatically when finalize_standard_phase is called — only when ctx.task_board.dirty is true.
Lifecycle
Section titled “Lifecycle”When .run() is called, the SDK:
- Reads
AgentMessageJSON from stdin - Parses and validates the message (version, type, required fields, signal_dir)
- Appends
{status: "ok"}to the JSONL signal channel - Resolves the handler key from the message
typefield - Calls the matched handler with the message
- Appends
{status: "complete"}or{status: "error"}to the signal channel - Exits with code 0 (success) or 1 (error)
No long-running server, no HTTP, no polling. The process starts, does its work, writes results, and exits.
Error handling
Section titled “Error handling”If your handler throws, the SDK:
- Appends
{status: "error", error: "<message>"}to the signal channel - Exits with code 1
The orchestrator reads the channel and escalates on error. You do not need to wrap your handler in a try/catch for protocol safety. However, you may want your own try/catch for retries or graceful degradation.
Directory Structure
Section titled “Directory Structure”A team with a custom agent:
my-team/├── team.yml├── roles/│ ├── research.md│ └── analysis.md├── agents/│ └── analyst/│ ├── index.ts│ └── package.json└── README.mdAfter cliq assemble:
.cliq/├── workflow.yml├── agents.json # generated manifest├── roles/│ ├── research.md│ └── analysis.md├── agents/│ └── analyst/│ ├── index.ts│ └── package.json├── channels/ # handoff envelopes between phases├── files/ # fetched/generated files, organized by phase│ └── {phase}/│ └── {name}└── signals/ # orchestrator coordination filesDiagnostics
Section titled “Diagnostics”cliq doctor reads each agent’s manifest.json to verify prerequisites:
- Binary check: reads the manifest’s entry point and verifies the agent is resolvable
- System binaries: reads
manifest.binariesand verifies each binary is available onPATH - Settings check: reads
settings.requiredfrom the manifest and verifies the keys are present inagents.<name>settings - Connectivity test: if the agent implements
test(), runs it to verify the agent can reach its external dependencies - With a project (
.cliq/exists): checks every agent referenced inagents.json - Without a project: checks only your default agent from
~/.cliqrc/settings.json
Checking dependencies...
tmux... ✓ (tmux 3.3a) node... ✓ (v20.11.0) git... ✓ (git version 2.43.0)
Agents: agent... ✓ (Cursor 0.45.0) api_key... ✓Configuration Reference
Section titled “Configuration Reference”| Field | Type | Default | Description |
|---|---|---|---|
default_agent | string | "cursor" | Default agent for phases without an explicit agent key |
agents.<name>.entry | string | — | Custom entry point for the agent |
agents.<name>.api_key | string | — | Agent API key (CLI and LLM API agents) |
agents.<name>.* | unknown | — | Arbitrary agent-specific settings (e.g. base_url, email, credentials_file) |
Agents receive their settings at runtime via ctx.settings. See Settings for the full settings reference.
Debugging
Section titled “Debugging”Agent process exits with code 1 — Your handler threw. Check the JSONL signal channel (<phase>.jsonl) in the signal directory for the error message. Common causes: missing API key, network timeout, undefined variable.
Empty or missing channels — If you don’t call ctx.send() for each target, downstream phases won’t find the expected handoff files. Use ctx.targets to see which phases expect output. Calling ctx.send() with an invalid target throws immediately.
AgentMessage parse error — The agent couldn’t parse the JSON from stdin. Check that the orchestrator is piping a valid AgentMessage envelope. Verify the JSON is well-formed and includes all required fields (version, type, agent, signal_dir).
ROUTE target validation error — The CliqAgent gate handler validates that ROUTE targets exist in ctx.route_targets. If your gate returns { outcome: 'ROUTE', target: 'nonexistent' }, the SDK throws and appends an error to the signal channel. Check your workflow’s depends_on graph — route targets are derived from predecessor phases.
Missing agent setting — If your custom agent requires an API key, configure it in settings.json under agents.<name> before running cliq run. The manifest’s settings.required array documents which keys are needed.
cliq settings agents.openai-analyst.api_key sk-... --globalcliq runAgent binary not found — A built-in agent’s underlying CLI binary must be on your PATH. Run cliq doctor to see which are missing and get install hints.
Connector Agent Patterns
Section titled “Connector Agent Patterns”Connector agents fetch data from external services or deliver results to them. They use msg.payload.action, msg.payload.sources, and msg.payload.target_entries to dispatch operations.
Action dispatch
Section titled “Action dispatch”Use msg.payload.action to route different operations within a single agent:
handlers: { 'phase:standard': async (msg: AgentMessage) => { const ctx = build_phase_context(msg, 'acme-connector'); const action = msg.payload.action as string || 'get'; const sources = msg.payload.sources as SourceEntry[] || []; const target_entries = msg.payload.target_entries as TargetEntry[] || [];
if (action === 'get' || action === 'list') { for (const source of sources) { const data = await fetch_from_api(source, action); ctx.write_file(source.name, data); } finalize_standard_phase(ctx, msg); return; }
if (action === 'create' || action === 'update') { for (const target of target_entries) { await deliver_to_api(target, ctx, action); } finalize_standard_phase(ctx, msg); return; }
throw new Error(`Unknown action: ${action}`); },},Different phases set different actions in team.yml:
- name: fetch-issues agent: acme-connector action: get sources: - name: report.json ref: "reports/latest"
- name: push-results agent: acme-connector action: create depends_on: [analyst] target_entries: - name: result file: analysis.json ref: "results/new"Testing connectors with a mock server
Section titled “Testing connectors with a mock server”The idiomatic pattern for connector tests is a local HTTP server + agent.dispatch(msg):
import { describe, it, expect, beforeEach, afterEach } from 'vitest';import http from 'node:http';import fs from 'node:fs';import path from 'node:path';import os from 'node:os';import { CliqAgent, build_phase_context, finalize_standard_phase } from '@cliqhub/cliq-sdk';import type { AgentMessage } from '@cliqhub/cliq-sdk';
describe('acme_connector', () => { let server: http.Server; let base_url: string; let project_dir: string; let last_path: string;
beforeEach(async () => { project_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cliq-test-'));
server = http.createServer((req, res) => { last_path = req.url || ''; res.setHeader('content-type', 'application/json'); res.writeHead(200); res.end(JSON.stringify({ status: 'ok', data: [1, 2, 3] })); });
await new Promise<void>(resolve => { server.listen(0, '127.0.0.1', () => { const addr = server.address() as { port: number }; base_url = `http://127.0.0.1:${addr.port}`; resolve(); }); });
const cliq = (...p: string[]) => path.join(project_dir, '.cliq', ...p); fs.mkdirSync(cliq('signals'), { recursive: true }); fs.mkdirSync(cliq('roles'), { recursive: true }); fs.mkdirSync(cliq('channels'), { recursive: true }); });
afterEach(async () => { await new Promise<void>(resolve => server.close(() => resolve())); fs.rmSync(project_dir, { recursive: true, force: true }); });
it('fetches source and writes to workspace', async () => { const msg: AgentMessage = { version: 1, type: 'phase', agent: 'acme-connector', instance_id: 'test-1', team_name: 'test', req_key: 'TEST-1', project_dir, work_dir: project_dir, signal_dir: path.join(project_dir, '.cliq', 'signals'), handoff_dirs: [], input_artifacts: [], settings: { base_url, api_key: 'test-key' }, payload: { phase_type: 'standard', phase: 'fetch-data', action: 'get', sources: [{ name: 'report.json', ref: 'reports/latest' }], }, };
const agent = new CliqAgent({ name: 'acme-connector', handlers: { 'phase:standard': async (m: AgentMessage) => { const ctx = build_phase_context(m, 'acme-connector'); const sources = m.payload.sources as { name: string; ref: string }[]; for (const source of sources || []) { const resp = await fetch(`${m.settings.base_url}/api/${source.ref}`, { headers: { 'Authorization': `Bearer ${m.settings.api_key}` }, }); ctx.write_file(source.name, await resp.text()); } finalize_standard_phase(ctx, m); }, }, });
await agent.dispatch(msg); expect(last_path).toBe('/api/reports/latest'); });});Publishing a custom agent
Section titled “Publishing a custom agent”Bundle a custom agent with your team:
- Create the agent directory with
manifest.jsonand agent code - Declare it in
team.ymlunderagents: - Reference it in phases with
agent: <name> - Set credentials via
cliq settings agents.<name>.<key> ... - Validate with
cliq doctor agent <name>andcliq team validate - Publish with
cliq team publish— the agent directory is bundled with the team package
See Also
Section titled “See Also”- Agent Reference — full agent catalog with handoff structures and configuration