Skip to content

Channels & Signals

cliq agents don’t share memory. Each agent runs in its own process with its own context window. There is no message bus, no shared database, no API layer between them. Instead, coordination happens through two filesystem-based mechanisms:

  • Channels — directories where agents write structured handoff documents for downstream phases
  • Signals — tiny files the orchestrator uses to track completion, verdicts, and pipeline state

This page covers both in detail.


Most multi-agent frameworks use message queues, shared memory, or API calls for inter-agent communication. cliq uses plain files. This is a deliberate choice, not a limitation.

Transparency. Every handoff between agents is a markdown file you can open and read. There’s no serialization format to decode, no message broker to query. When something goes wrong, you read the file.

Auditability. The .cliq/ directory is a complete record of what happened. Channels show what each agent told the next. Signals show when each phase completed. You can reconstruct the entire pipeline run from the filesystem alone.

Portability. Channels and signals work anywhere there’s a filesystem — your laptop, a CI runner, a container, a remote server. No daemon to install, no port to bind.

Debuggability. You can inspect, edit, or even pre-populate channel files mid-run. If an agent wrote a bad handoff, fix the file and re-run the downstream phase. The filesystem is your API.


A channel is a directory under .cliq/channels/ dedicated to one edge in the workflow DAG. When phase A depends on phase B, there is a channel directory where B writes handoff notes for A to read.

Channel directories follow the pattern:

{from_phase}--{to_phase}

The from phase is the writer; the to phase is the reader. The double-dash -- separator is used instead of a single dash because phase names themselves may contain hyphens (e.g., security-auditor).

Examples:

architect--developer # architect writes handoff for developer
architect--security-auditor # architect writes handoff for security-auditor
developer--reviewer # developer writes handoff for reviewer
reviewer--developer # reviewer writes feedback to developer (gate route)

The orchestrator creates channel directories at pipeline startup, before any agent runs. The ChannelManager.create_channels() method walks the workflow DAG’s dependency edges and creates one directory per edge:

for (const edge of edges) {
const channel_name = `${edge.from}--${edge.to}`;
fs.mkdirSync(path.join(channels_dir, channel_name), { recursive: true });
}

Gate phases get additional bidirectional channels created via ChannelManager.create_gate_channels() — more on that below.

The result: every possible handoff path has a directory ready before the first agent starts.

When the orchestrator activates a phase, the prompt generator builds a prompt that tells the agent exactly which channels to read and write.

Incoming channels. For each dependency (predecessor phase), the prompt includes:

Read your incoming channels:
- .cliq/channels/architect--developer/ (handoff from Architect)
- .cliq/channels/tester--developer/ (handoff from Tester)
Read any instructions.md files in those channels if present.

The agent reads these directories, finds handoff files written by predecessor phases, and uses that context to inform its work.

Outgoing channels. The SDK automatically writes a structured handoff.json envelope to each outgoing channel directory when the agent calls ctx.send(). The agent doesn’t interact with channel paths directly.

When a phase has multiple successors in the DAG, it writes to multiple outgoing channels. Each handoff can — and should — be tailored to the downstream agent’s needs.

For example, an architect phase with both a developer and a security-auditor downstream:

.cliq/channels/
├── architect--developer/
│ └── handoff.json # Implementation plan, API contracts, file structure
├── architect--security-auditor/
│ └── handoff.json # Threat model, auth boundaries, data flow concerns

The architect writes different content to each channel. The developer gets an implementation plan; the security auditor gets a threat model. Same source phase, targeted handoffs.

Gate Channels: Bidirectional Communication

Section titled “Gate Channels: Bidirectional Communication”

Gate phases (like a reviewer) create bidirectional channels between the gate and every phase it can route work to. This is handled by ChannelManager.create_gate_channels(), which creates both the forward and reverse channel:

.cliq/channels/
├── developer--reviewer/ # developer → reviewer (normal DAG flow)
└── reviewer--developer/ # reviewer → developer (gate feedback)

When the reviewer gate routes work back to the developer, it writes a structured verdict envelope into reviewer--developer/handoff.json — including what failed, check results, and context. The developer reads this feedback on its next activation, fixes the issues, and writes back to developer--reviewer/ so the gate can re-evaluate.

This feedback loop can repeat up to the gate’s max_iterations budget (typically 3).

Any channel directory can contain an instructions.md file with role-specific guidance for that handoff. These are included in the agent’s prompt context and read before the agent produces its handoff.

.cliq/channels/architect--developer/
├── instructions.md # "Include file paths, API signatures, and test expectations"
└── handoff.json # The structured handoff (written by the SDK at runtime)

Channel instructions are optional. When present, they shape the handoff’s format and content. You can use them to enforce structure — requiring specific sections, file path references, or acceptance criteria.

If a channel directory exists but contains no handoff files, the downstream agent simply has no predecessor context for that edge. The agent proceeds with whatever other context it has (role instructions, task board, design docs, other incoming channels).

If a channel directory doesn’t exist at all, it won’t appear in the agent’s prompt — the prompt generator only includes channels for edges that exist in the DAG.

Neither case is an error. Phases without predecessors (like the first phase in a pipeline) naturally have no incoming channels.

Every handoff.json file contains a structured envelope:

{
"version": 1,
"phase_type": "standard",
"phase": "analyzer",
"agent": "cursor",
"data": { "environment": "staging", "version": "2.1.0" },
"text": "Analysis complete. Deploy to staging with version 2.1.0."
}
FieldTypeDescription
versionnumberEnvelope format version (always 1).
phase_typestringDiscriminator: "standard", "gate", or "team".
phasestringName of the phase that wrote this handoff.
agentstringName of the agent that ran.
dataobject?Structured payload for programmatic consumption. Accessible via $(handoff.<phase>.<path>) template variables.
textstring?Natural language summary for LLM consumption.

For LLM agents, the SDK extracts both data and text from the agent’s output automatically. The text field is injected into downstream agent prompts. The data field is available for template variable resolution.

For connector agents (S3, Jira, Google Drive, curl, Confluence, Zendesk, HubSpot, Datadog), the data field carries enriched structured results in name-keyed maps — including URLs, file paths, sizes, and service-specific metadata. Fetched content is written to .cliq/files/{phase}/{name} and referenced in the file field. This means downstream phases can access both the raw data on disk and the metadata (like a Jira issue’s URL, status, and summary) via template variables or the SDK. See the Agent Reference for the exact data shape each connector produces.

For exec agents and custom SDK agents, the data field carries command results or whatever structured payload the agent produces.

Gate envelopes (phase_type: "gate") nest verdict information inside data:

{
"version": 1,
"phase_type": "gate",
"phase": "reviewer",
"agent": "cursor",
"data": {
"verdict": { "outcome": "ROUTE", "target": "developer", "reason": "Tests fail." },
"checks": [{ "name": "npm test", "pass": false }],
"iteration": 1,
"max_iterations": 3
},
"text": "ROUTE to developer: Tests fail due to missing error handling."
}

See Template Variables — $(handoff.*) for wiring handoff data into downstream YAML fields.


Signals coordinate the pipeline between agents and the orchestrator. The primary mechanism is a JSONL signal channel — a per-phase append-only log file at .cliq/signals/<phase>.jsonl. A small number of standalone signal files remain for run-level state.

Agents don’t manage signals directly — the SDK handles all signal operations transparently.

Each phase gets a single JSONL file. The agent appends structured messages; the orchestrator reads them by polling.

A message in the channel (a ChannelMessage) looks like:

{"ts":"2026-05-29T06:04:14.222Z","version":1,"type":"phase","status":"ok"}
{"ts":"2026-05-29T06:04:38.105Z","version":1,"type":"phase","status":"complete","result":{...}}
FieldTypeDescription
tsstringISO-8601 timestamp
version1Protocol version
typestringDispatch type: "phase", "notify", or "test"
statusstringLifecycle status (see below)
resultunknown?Handler return value (present on complete)
errorstring?Error description (present on error)
messagestring?Progress note (present on progress)

Status values:

StatusMeaningTerminal?
okAgent received the message and resolved its handlerNo
progressIntermediate status updateNo
notifyNotification dispatch request for the orchestratorNo
completeHandler finished successfullyYes
errorHandler failedYes

The protocol is: exactly one ok (ack), followed by zero or more non-terminal messages (progress, notify), followed by exactly one terminal message (complete or error).

A few standalone signal files remain for state that isn’t phase-scoped or needs to be observable by external tools:

Signal fileWritten byContentsPurpose
{phase}_routedOrchestratorEmpty fileSupport phase was activated by a gate route
{phase}_command_results.jsonOrchestratorJSON array of check resultsGate/exec command outcomes
_pipeline_statusOrchestratorCOMPLETED, ESCALATED, FAILED, or CANCELLEDFinal pipeline outcome
  1. Clear — The orchestrator deletes the previous JSONL channel file to start fresh.

  2. Activate — The orchestrator writes an AgentMessage to a temp file and spawns the agent in a tmux pane (piping the message via stdin).

  3. Agent acks — The SDK reads the message from stdin, resolves the handler, and appends {status: "ok"} to the channel.

  4. Agent works — The handler runs (calling a CLI, making API requests, etc.). It may append progress or notify messages during execution.

  5. Agent completes — The SDK appends {status: "complete", result: {...}} (or {status: "error", error: "..."} on failure).

  6. Orchestrator detects — The orchestrator polls the JSONL every 3 seconds, reading all messages since its last check. When it sees a terminal status, it knows the phase is done.

  7. Advance — The orchestrator checks which phases are now ready and activates the next batch.

  1. Orchestrator runs automated checks — Before the gate agent starts, configured commands (tests, linters) are run and results recorded.

  2. Orchestrator clears channel — The previous iteration’s JSONL is deleted.

  3. Gate agent evaluates — The gate agent receives check results and route targets, evaluates the work, and returns a verdict.

  4. SDK writes completion — The SDK appends {status: "complete", result: { verdict: { outcome, target?, reason } }} to the channel.

  5. Orchestrator reads verdict — The orchestrator reads the complete message and extracts the verdict from result.verdict.

  6. Branch on outcome:

    • PASS — Gate is done. Pipeline continues to post-gate phases.
    • ROUTE — Orchestrator activates the target phase, waits for completion, then loops back to step 1.
    • ESCALATE — Pipeline halts. Human intervention required.
  7. Budget check — If the gate exhausts its max_iterations budget without passing, the orchestrator auto-escalates.

Agents can request notification delivery by appending a notify message to the channel:

{"ts":"...","version":1,"type":"phase","status":"notify","result":{"agent":"slack","channel":"elan","event":"hug_review","payload":{...}}}

The orchestrator dispatches the notification to the target agent (e.g., spawning the slack agent) and continues polling. This is used by the HUG agent to send review requests and reminders.

ParameterValueDescription
POLL_INTERVAL3 secondsHow often the orchestrator reads the JSONL channel
PHASE_TIMEOUT20 minutesMax time a standard phase can run before escalation
GATE_AGENT_TIMEOUT15 minutesMax time a gate agent can take to produce a verdict

If a phase times out without writing a terminal message, the orchestrator escalates. If the agent’s tmux pane dies without writing to the channel, the orchestrator detects the dead pane and escalates immediately.

When a user runs cliq cancel or sends SIGINT/SIGTERM to the orchestrator:

  1. The orchestrator writes CANCELLED to .cliq/signals/_pipeline_status
  2. Every poll loop checks for cancellation before reading the channel
  3. If cancelled, the orchestrator cleans up and exits

Cancellation is detected within one POLL_INTERVAL (3 seconds).


Here’s the complete .cliq/ directory structure for a pipeline with four phases (architect → developer → reviewer gate → developer support loop):

.cliq/
├── channels/
│ ├── architect--developer/
│ │ ├── instructions.md # (optional) guidance for this handoff
│ │ └── handoff.json # architect's structured handoff for the developer
│ ├── architect--security-auditor/
│ │ └── handoff.json # architect's threat model notes
│ ├── developer--reviewer/
│ │ └── handoff.json # developer's summary for the reviewer
│ └── reviewer--developer/
│ └── handoff.json # reviewer's verdict and feedback (gate route)
├── files/ # fetched/generated files from connector agents
│ └── fetch-data/ # organized by phase name
│ ├── config.json
│ └── report.csv
├── signals/
│ ├── architect.jsonl # JSONL channel — ok + complete
│ ├── developer.jsonl # JSONL channel — ok + complete
│ ├── security-auditor.jsonl # JSONL channel — ok + complete
│ ├── reviewer.jsonl # JSONL channel — ok + complete (with verdict in result)
│ ├── reviewer_command_results.json # gate check command outcomes
│ ├── developer_routed # empty — developer was re-activated by gate
│ └── _pipeline_status # "COMPLETED"
├── roles/
│ ├── architect.md
│ ├── developer.md
│ ├── security-auditor.md
│ └── reviewer.md
├── design/ # shared design documents
├── task_board.md # shared task board
└── orchestrator.log # full orchestrator output

Channels are just directories with JSON files. You can read them at any time, even while the pipeline is running:

Terminal window
# See all channels
ls .cliq/channels/
# Read a specific handoff
cat .cliq/channels/architect--developer/handoff.json
# Check if a channel has content
ls -la .cliq/channels/developer--reviewer/

If a downstream agent is behaving unexpectedly, check its incoming channels first — the handoff it received may be incomplete or misleading.

Terminal window
# See all signals
ls -la .cliq/signals/
# Read a phase's JSONL channel (look for complete/error)
cat .cliq/signals/developer.jsonl
# Check if a phase completed
grep '"complete"' .cliq/signals/developer.jsonl
# Read the gate verdict (in the complete message's result)
cat .cliq/signals/reviewer.jsonl | grep '"complete"'
# Check pipeline status
cat .cliq/signals/_pipeline_status

Phase never completes. The agent process may have crashed or the implementation may have hung. Check the agent’s tmux pane output. If the JSONL only has an ok message and the pane is dead, the agent crashed after ack but before completing. The orchestrator detects dead panes and escalates.

Gate stuck in a loop. The gate keeps routing to the same phase without progress. Check the orchestrator log for iteration counts. If approaching max_iterations, the gate will auto-escalate.

Stale signals from a previous run. The orchestrator clears the JSONL channel before activating each phase, so stale messages are not normally an issue. If you’re debugging manually:

Terminal window
rm .cliq/signals/*.jsonl
rm .cliq/signals/*_routed
rm .cliq/signals/_pipeline_status

Channel handoff is empty or wrong. If a predecessor phase wrote a poor handoff, you can edit the file directly and re-run the downstream phase. The channel file is the input — fix the input, get better output.


Consider a simple pipeline: architect → developer → reviewer (gate).

phases:
- name: architect
type: standard
- name: developer
type: standard
depends_on: [architect]
- name: reviewer
type: gate
depends_on: [developer]
max_iterations: 3

Step 1: Orchestrator starts. Creates channels and clears signals.

.cliq/channels/architect--developer/ (empty)
.cliq/channels/developer--reviewer/ (empty)
.cliq/channels/reviewer--developer/ (empty, gate route channel)

Step 2: Architect runs. No incoming channels (first phase). The SDK calls ctx.send() which writes:

.cliq/channels/architect--developer/handoff.json
{
"version": 1,
"phase_type": "standard",
"phase": "architect",
"agent": "cursor",
"text": "Implement the REST API with three endpoints...",
"data": {
"approach": "REST API with Express + SQLite",
"endpoints": ["GET /items", "POST /items", "DELETE /items/:id"],
"files": ["src/routes/items.ts", "src/models/item.ts", "src/db/connection.ts"]
}
}

SDK appends {status: "complete"} to architect.jsonl. Orchestrator detects it, kills the architect process.

Step 3: Developer runs. Reads architect’s handoff, implements the plan, calls ctx.send():

.cliq/channels/developer--reviewer/handoff.json
{
"version": 1,
"phase_type": "standard",
"phase": "developer",
"agent": "cursor",
"text": "Implemented all three endpoints with zod validation and 12 unit tests.",
"data": {
"files_created": ["src/routes/items.ts", "src/models/item.ts", "src/db/connection.ts"],
"test_count": 12
}
}

SDK appends {status: "complete"} to developer.jsonl. Orchestrator detects it, moves to the gate.

Step 4: Reviewer gate (iteration 1). Orchestrator runs checks (tests, linter). Tests fail. Orchestrator builds gate_context.md, launches the gate agent. The gate SDK writes:

.cliq/channels/reviewer--developer/handoff.json
{
"version": 1,
"phase_type": "gate",
"phase": "reviewer",
"agent": "cursor",
"text": "ROUTE to developer: 2 of 12 tests fail — DELETE endpoint returns 500 instead of 404.",
"data": {
"verdict": { "outcome": "ROUTE", "target": "developer", "reason": "DELETE returns 500 instead of 404 for missing items." },
"checks": [{ "name": "npm test", "pass": false }],
"iteration": 1,
"max_iterations": 3
}
}

The SDK also appends {status: "complete", result: { verdict: { outcome: "ROUTE", target: "developer", ... }}} to reviewer.jsonl.

Step 5: Developer runs again (routed). Reads reviewer--developer/handoff.json, fixes the bug, writes back to developer--reviewer/, SDK appends {status: "complete"} to developer.jsonl.

Step 6: Reviewer gate (iteration 2). Orchestrator re-runs checks. All tests pass. Gate agent writes a PASS verdict. SDK appends {status: "complete", result: { verdict: { outcome: "PASS" }}} to reviewer.jsonl.

Orchestrator writes COMPLETED to _pipeline_status. Pipeline done.