Comma Agents
@comma-agents/coreFlowsBuilt-in

createCycleFlow

Repeated pipeline iterations with optional observer feedback and break signals between cycles.

createCycleFlow repeats a sequential pipeline N times. Each cycle runs all steps in order; the output of one cycle becomes the input of the next. An optional observer agent can provide feedback between cycles and signal when to stop.

import { createAgent, createCycleFlow } from "@comma-agents/core";

const writer = createAgent({
  name: "writer",
  model: "openai/gpt-4o",
  systemPrompt: "Write or refine a poem based on the input.",
});

const reviewer = createAgent({
  name: "reviewer",
  model: "openai/gpt-4o",
  systemPrompt: "Critique the poem and suggest improvements.",
});

const flow = createCycleFlow({
  name: "poem-refinement",
  steps: [writer, reviewer],
  cycles: 3,
});

const result = await flow.call("Write a haiku about TypeScript.");
console.log(result.text);

CycleFlowConfig

Prop

Type

Cycle Behavior

Each cycle runs all steps sequentially (same as createSequentialFlow). The output of the last step in cycle N becomes the input for the first step in cycle N+1:

Cycle 1: writer("topic") → reviewer(writer's output)
Cycle 2: writer(reviewer's output from cycle 1) → reviewer(writer's output)
Cycle 3: writer(reviewer's output from cycle 2) → reviewer(writer's output)

The default is 1 cycle. Set cycles: Infinity for an unbounded loop — use AbortablePromise.abort() to stop it:

const flow = createCycleFlow({
  name: "infinite-loop",
  steps: [writer, reviewer],
  cycles: Infinity,
});

const promise = flow.call("topic");
// Cancel after 30 seconds
setTimeout(() => promise.abort(), 30_000);

Observer

The observer option adds an agent that runs after each cycle's steps complete, before the next cycle begins. The observer's output becomes the input for the next cycle:

const observer = createAgent({
  name: "observer",
  model: "openai/gpt-4o",
  systemPrompt: "Summarize the feedback so far into a clear directive for the next iteration.",
});

const flow = createCycleFlow({
  name: "observed-refinement",
  steps: [writer, reviewer],
  cycles: 3,
  observer,
});

The observer runs after all steps complete but before any user-provided alterMessageAfterCycle hooks. Calling reset() on the flow also resets the observer.

Breaking the Cycle

The observer can signal the cycle to stop early by including a break signal in its response. The default break signals are:

  • "end cycle"
  • "stop"
  • "done"

Matching is case-insensitive and checks if the response contains the phrase (not exact match):

const critic = createAgent({
  name: "critic",
  model: "openai/gpt-4o",
  systemPrompt: `Review the output. If it's satisfactory, respond with "done". Otherwise, suggest improvements.`,
});

const flow = createCycleFlow({
  name: "refine-until-done",
  steps: [writer],
  cycles: Infinity, // Run until critic says "done"
  observer: critic,
});

When a break signal is detected:

  • The cycle loop stops immediately
  • The returned output is the step output (before the observer ran), not the observer's break message
  • Any remaining cycles are skipped

You can customize the break signals via breakCycleSignals:

const flow = createCycleFlow({
  name: "custom-break",
  steps: [writer],
  cycles: Infinity,
  observer: critic,
  breakCycleSignals: ["finished", "abort", "complete"],
});

Cycle Hooks

Cycle flows support two additional hooks beyond the standard flow hooks. Use hookIntoFlow<CycleHooks>() to attach them:

import { createCycleFlow, hookIntoFlow } from "@comma-agents/core";
import type { CycleHooks } from "@comma-agents/core";

const flow = createCycleFlow({
  name: "loop",
  steps: [writer],
  cycles: 3,
});

hookIntoFlow<CycleHooks>(flow, {
  alterMessageBeforeCycle: [
    async (message) => `[cycle-input] ${message}`,
  ],
  alterMessageAfterCycle: [
    async (message) => `${message} [cycle-done]`,
  ],
});

See the Flow Hooks page for the full hook reference.

On this page