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.
Why the Filesystem?
Section titled “Why the Filesystem?”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.
Channels
Section titled “Channels”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.
Naming Convention
Section titled “Naming Convention”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 developerarchitect--security-auditor # architect writes handoff for security-auditordeveloper--reviewer # developer writes handoff for reviewerreviewer--developer # reviewer writes feedback to developer (gate route)How Channels Are Created
Section titled “How Channels Are Created”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.
How Channel Content Flows into Prompts
Section titled “How Channel Content Flows into Prompts”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.
Fan-Out: One Phase, Multiple Successors
Section titled “Fan-Out: One Phase, Multiple Successors”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 concernsThe 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).
Channel Instructions
Section titled “Channel Instructions”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.
Empty and Missing Channels
Section titled “Empty and Missing Channels”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.
The Handoff Envelope
Section titled “The Handoff Envelope”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."}| Field | Type | Description |
|---|---|---|
version | number | Envelope format version (always 1). |
phase_type | string | Discriminator: "standard", "gate", or "team". |
phase | string | Name of the phase that wrote this handoff. |
agent | string | Name of the agent that ran. |
data | object? | Structured payload for programmatic consumption. Accessible via $(handoff.<phase>.<path>) template variables. |
text | string? | 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
Section titled “Signals”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.
The JSONL Signal Channel
Section titled “The JSONL Signal Channel”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":{...}}| Field | Type | Description |
|---|---|---|
ts | string | ISO-8601 timestamp |
version | 1 | Protocol version |
type | string | Dispatch type: "phase", "notify", or "test" |
status | string | Lifecycle status (see below) |
result | unknown? | Handler return value (present on complete) |
error | string? | Error description (present on error) |
message | string? | Progress note (present on progress) |
Status values:
| Status | Meaning | Terminal? |
|---|---|---|
ok | Agent received the message and resolved its handler | No |
progress | Intermediate status update | No |
notify | Notification dispatch request for the orchestrator | No |
complete | Handler finished successfully | Yes |
error | Handler failed | Yes |
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).
Remaining File-Per-Signal Conventions
Section titled “Remaining File-Per-Signal Conventions”A few standalone signal files remain for state that isn’t phase-scoped or needs to be observable by external tools:
| Signal file | Written by | Contents | Purpose |
|---|---|---|---|
{phase}_routed | Orchestrator | Empty file | Support phase was activated by a gate route |
{phase}_command_results.json | Orchestrator | JSON array of check results | Gate/exec command outcomes |
_pipeline_status | Orchestrator | COMPLETED, ESCALATED, FAILED, or CANCELLED | Final pipeline outcome |
Signal Lifecycle — Standard Phases
Section titled “Signal Lifecycle — Standard Phases”-
Clear — The orchestrator deletes the previous JSONL channel file to start fresh.
-
Activate — The orchestrator writes an
AgentMessageto a temp file and spawns the agent in a tmux pane (piping the message via stdin). -
Agent acks — The SDK reads the message from stdin, resolves the handler, and appends
{status: "ok"}to the channel. -
Agent works — The handler runs (calling a CLI, making API requests, etc.). It may append
progressornotifymessages during execution. -
Agent completes — The SDK appends
{status: "complete", result: {...}}(or{status: "error", error: "..."}on failure). -
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.
-
Advance — The orchestrator checks which phases are now ready and activates the next batch.
Gate Signal Flow
Section titled “Gate Signal Flow”-
Orchestrator runs automated checks — Before the gate agent starts, configured commands (tests, linters) are run and results recorded.
-
Orchestrator clears channel — The previous iteration’s JSONL is deleted.
-
Gate agent evaluates — The gate agent receives check results and route targets, evaluates the work, and returns a verdict.
-
SDK writes completion — The SDK appends
{status: "complete", result: { verdict: { outcome, target?, reason } }}to the channel. -
Orchestrator reads verdict — The orchestrator reads the
completemessage and extracts the verdict fromresult.verdict. -
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.
-
Budget check — If the gate exhausts its
max_iterationsbudget without passing, the orchestrator auto-escalates.
Notifications via the Channel
Section titled “Notifications via the Channel”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.
Poll Interval and Timeouts
Section titled “Poll Interval and Timeouts”| Parameter | Value | Description |
|---|---|---|
POLL_INTERVAL | 3 seconds | How often the orchestrator reads the JSONL channel |
PHASE_TIMEOUT | 20 minutes | Max time a standard phase can run before escalation |
GATE_AGENT_TIMEOUT | 15 minutes | Max 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.
Pipeline Cancellation
Section titled “Pipeline Cancellation”When a user runs cliq cancel or sends SIGINT/SIGTERM to the orchestrator:
- The orchestrator writes
CANCELLEDto.cliq/signals/_pipeline_status - Every poll loop checks for cancellation before reading the channel
- If cancelled, the orchestrator cleans up and exits
Cancellation is detected within one POLL_INTERVAL (3 seconds).
Directory Layout
Section titled “Directory Layout”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 outputDebugging
Section titled “Debugging”Reading Channels During a Run
Section titled “Reading Channels During a Run”Channels are just directories with JSON files. You can read them at any time, even while the pipeline is running:
# See all channelsls .cliq/channels/
# Read a specific handoffcat .cliq/channels/architect--developer/handoff.json
# Check if a channel has contentls -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.
Checking Signal State
Section titled “Checking Signal State”# See all signalsls -la .cliq/signals/
# Read a phase's JSONL channel (look for complete/error)cat .cliq/signals/developer.jsonl
# Check if a phase completedgrep '"complete"' .cliq/signals/developer.jsonl
# Read the gate verdict (in the complete message's result)cat .cliq/signals/reviewer.jsonl | grep '"complete"'
# Check pipeline statuscat .cliq/signals/_pipeline_statusCommon Issues
Section titled “Common Issues”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:
rm .cliq/signals/*.jsonlrm .cliq/signals/*_routedrm .cliq/signals/_pipeline_statusChannel 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.
Example: A Three-Phase Pipeline
Section titled “Example: A Three-Phase Pipeline”Consider a simple pipeline: architect → developer → reviewer (gate).
Workflow
Section titled “Workflow”phases: - name: architect type: standard - name: developer type: standard depends_on: [architect] - name: reviewer type: gate depends_on: [developer] max_iterations: 3Run Trace
Section titled “Run Trace”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:
{ "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():
{ "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:
{ "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.