Groups

Groups organize LLM calls into logical phases within a run. They give your agent's execution tree structure and make it easy to see what happened at each step.

What is a group?

A group represents a phase, step, or logical unit within a run. If a run is the "what" (the task), groups are the "how" (the steps the agent took to complete it).

Groups are optional. For simple agents with a single LLM call, you can link calls directly to the run. But for multi-step agents, groups provide crucial visibility into which phase produced which calls, how long each phase took, and where failures occur.

ID format: wm_grp_<ulid>

When to use groups

Use groups when your agent has distinct phases. Some patterns:

Plan → Execute
A planning group that decides what to do, then an execution group that does it.
Analyze → Synthesize
One group gathers information, another produces the final output.
Triage → Route → Handle
Classification step, routing decision, then specialized handling.
Research → Draft → Review
Multi-pass content generation with self-review.
Validate → Transform → Output
Data processing pipelines with validation steps.

Labels

Like runs, groups have a label for categorization. Groups with the same label are aggregated across runs, so you can see how your "planning" phase performs across all executions.

Group label conventions
// Labels describe the phase, not the instance
group(r, 'Planning');
group(r, 'Code generation');
group(r, 'Review');

API

group(target: Run | Group, label: string, opts?: object): Group

Creates a new group linked to a run or parent group. Returns a frozen group handle.

Parameters
target— The parent run or group. The new group is automatically linked as a child.
label— Category name for this group. Groups with the same label are aggregated in dashboards.
opts— Optional metadata object.
Returns

A frozen object with id and _type: 'group'. Pass this to call(), outcome(), or nest further with another group().

Basic example

Two-phase agent
import OpenAI from 'openai';
import { warp, run, group, call, outcome, flush } from '@warpmetrics/warp';

const openai = warp(new OpenAI());

const r = run('Code review');

// Planning phase
const planning = group(r, 'Planning');
const planRes = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'What should we check in this PR?' }],
});
call(planning, planRes);

// Execution phase
const execution = group(r, 'Execution');
const execRes = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'Review the code for these issues...' }],
});
call(execution, execRes);

outcome(r, 'Completed');
await flush();

Nesting groups

Groups can be nested to any depth. Pass a group as the target to create a sub-group. This is useful for complex agents with nested control flow.

Nested groups
const r = run('Data pipeline');

const validation = group(r, 'Validation');
const schemaCheck = group(validation, 'Schema check');
call(schemaCheck, await openai.chat.completions.create({...}));

const dataCheck = group(validation, 'Data quality');
call(dataCheck, await openai.chat.completions.create({...}));

const transform = group(r, 'Transform');
call(transform, await openai.chat.completions.create({...}));

This produces a tree like:

Run (Data pipeline)
├── Validation
│   ├── Schema check
│   │   └── gpt-4o call
│   └── Data quality
│       └── gpt-4o call
└── Transform
    └── gpt-4o call

Dynamic branching

Groups work naturally with dynamic control flow. Create different groups depending on the agent's decisions — Warpmetrics captures whichever path is taken.

Branching execution
const r = run('Support agent');

// Triage
const triage = group(r, 'Triage');
const triageRes = await openai.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: 'Classify this ticket...' }],
});
call(triage, triageRes);

const category = triageRes.choices[0].message.content;

// Branch based on classification
if (category === 'billing') {
  const billing = group(r, 'Billing handler');
  call(billing, await openai.chat.completions.create({...}));
} else if (category === 'technical') {
  const tech = group(r, 'Technical handler');
  const research = group(tech, 'Research');
  call(research, await openai.chat.completions.create({...}));
  const response = group(tech, 'Response');
  call(response, await openai.chat.completions.create({...}));
}

In the dashboard, you can use Show all paths to see which branches exist across all runs with the same label, including branches the current run didn't take.

Outcomes on groups

You can record outcomes on individual groups, not just runs. This lets you track success/failure at the phase level.

Group-level outcomes
const validation = group(r, 'Validation');
// ... calls ...

if (isValid) {
  outcome(validation, 'Passed');
} else {
  outcome(validation, 'Failed', { errors: validationErrors });
}

Tips

-Groups are optional. For simple single-call agents, link calls directly to the run.
-Keep labels consistent. If you sometimes call a phase 'Planning' and sometimes 'Plan', they'll appear as separate groups in dashboards.
-Use nesting sparingly. One or two levels deep is usually enough. Deep nesting makes trees harder to read.
-Groups are created instantly with no performance overhead. The SDK queues events in memory.