Groups and segments

A segment is a saved query that recomputes every time, and a group is a named set of identities that pipelines add to, gate on, and source from

What you'll learn

You want one pipeline to decide who's worth working and another to work them, or you need a contact rule that holds across every campaign, or want a saved list that updates itself.

  • how a segment differs from a group, and when to use each
  • how a two-stage campaign hands records off through a group with limit and once
  • what a suppression group blocks that delivery idempotency lets through
  • why a group keeps its first answer when a segment recomputes

Here's one campaign split into two pipelines. The first decides who qualifies:

The second works the qualified people, one per run:

name: send
version: 1

source:
  group: qualified
  limit: 1
  once: true

steps:
  - id: out
    use: csv/deliver
    with:
      path: out.csv
    variables:
      name: full_name
    idempotency: email
    record: contacted

Save both next to the contacts.csv from See it run, which has three people: Jane, Bob, and Carol. Everything on this page spends $0 and sends nothing, because the only target is a local CSV file. Run the first against a scratch ledger:

export GTME_LEDGER=$(mktemp -d)/ledger.db
gtme run qualify.yaml

The output is similar to the following:

...
step       adapter     in  out  empty  cached  filtered  failed  cost  avoided
source     csv/source  0   3    -      0       -         -       $0    -
marketers  sql/filter  3   2    -      0       1         -       $0    -
group "qualified": 2 record(s) added
total: $0 spent

Then run the second:

gtme run send.yaml

The output is similar to the following:

run 01M3FBCT5TRJBD1MTJ0PKVG09G (send)
source: sourced 1 members of group "qualified" (2 of 2 not yet worked; limit 1, oldest first)
...
step    adapter          in  out  empty  cached  filtered  failed  cost  avoided
source  group:qualified  0   1    -      0       -         -       $0    -
out     csv/deliver      1   1    -      0       -         -       $0    -
total: $0 spent

Run gtme run send.yaml again and it writes Carol. A third run sources nobody:

source: sourced 0 members of group "qualified" (0 of 2 not yet worked; limit 1, oldest first)

Here's what the three runs left behind:

gtme groups
group      type    members  added  removed  touched  created
contacted  person  0        0      0        2        2026-09-26
qualified  person  2        2      0        0        2026-09-26

What you just saw

A group is a named set of identities, stored in the ledger. The last line, group: qualified, adds every record that finishes the run. Bob's title failed the marketers filter, so the receipt line under the table says 2 were added.

source: {group: qualified} makes the group a pipeline's source. limit: 1 serves at most one member per run, oldest-added first.

once: true skips members this pipeline already finished, meaning they completed its last step or a filter stopped them. A record that failed a step stays eligible and gets retried. Without once:, every run would serve Jane.

That source: block is the piece you'd hand your agent: a bounded read that advances through a set another pipeline already decided.

gtme groups lists every group. members is current membership. added and removed count membership events, and touched counts deliveries recorded under the group.

The out step's record: contacted appends a touched event per successful delivery, and the first one created contacted. A touch isn't membership, which is why contacted has 2 touches and 0 members. Without record:, touches go to a group named after the pipeline.

gtme groups show says where a group sits in the chain.

group qualified (person) — 2 member(s)
  person:carol@initech.dev
  person:jane.doe@acme.com
written by:  qualify
sourced by:  send
recent events:
  2026-09-26 17:14  added    carol@initech.dev  {"pipeline":"qualify"}
  2026-09-26 17:14  added    jane.doe@acme.com  {"pipeline":"qualify"}

The first lines are the members by identity key. written by and sourced by are the pipelines that added to the group and read from it.

Groups also gate steps. require: [qualified] and exclude: [contacted] work on any step after the source. An ai/filter step with exclude: set to the group it writes judges each person once.

Put a review in the middle

A group/deliver step lets you approve the handoff before anyone joins the group. Replace the last line of qualify.yaml, group: qualified, with this step and save the file as qualify-review.yaml:

  - id: handoff
    use: group/deliver
    with: { group: qualified }
    variables:
      name: full_name
      title: title
gtme run qualify-review.yaml --dry-run

The output is similar to the following:

...
handoff: 2 record(s) would be handed off to group "qualified" (held back — dry run)
handoff: resolved variables for 2 record(s) — review, then run again without --dry-run to arm:
  jane.doe@acme.com
    name: "Jane Doe"
    title: "VP Marketing"
  carol@initech.dev
    name: "Carol Reyes"
    title: "Marketing Operations Manager"
total: $0 spent

The armed run, without the flag, prints:

handoff: 2 record(s) handed off to group "qualified"

A dry-run holds the handoff and prints who would be added, and the armed run is the approval. A group/deliver and a network delivery never share a pipeline, or approving one would approve both.

To turn someone away after arming, remove them from the group with a reason:

gtme groups remove qualified carol@initech.dev --note "not a fit"

To keep them out before the handoff, narrow with a sql/filter step ahead of it.

On the other side, limit: 50 with once: true works the group 50 a run. gtme has no scheduler, so cron or your agent runs send.yaml daily, as Run on cron and events shows.

Suppression groups

A suppression group blocks a second contact on any target. Delivery idempotency only stops a repeat to the same file or campaign. contacted already exists, because send.yaml recorded to it. Here's the step of a followup.yaml that sources qualified with no limit and writes followup.csv:

  - id: nudge
    use: csv/deliver
    with:
      path: followup.csv
    variables:
      name: full_name
    idempotency: email
    suppress: { group: contacted, within: 30d }
gtme run followup.yaml

The output is similar to the following:

...
nudge: 2 record(s) suppressed:
  jane.doe@acme.com: touched in "contacted" 15s ago
  carol@initech.dev: touched in "contacted" 15s ago
total: $0 spent

Without the suppress: line, the same run writes both rows. A suppressed record gets a fail verdict at that step, the kind a filter writes and the ledger keeps. The record still moves on to later steps, because suppression withholds only the send. The receipt lists each one with the age of the touch that blocked it. within: is required, so every suppression rule has a window.

Segments

A segment is a saved read-only query, and it runs fresh every time.

gtme query --save marketers "SELECT v.identity_id, i.identity_key \
    FROM current_values v JOIN identities i ON i.id = v.identity_id \
    WHERE v.field = 'title' AND v.value LIKE '%Marketing%'"
{"identity_id":"01M3FBCP00C754EHAPF8RF0H0N","identity_key":"jane.doe@acme.com"}
{"identity_id":"01M3FBCP02TGBGEKJWSMHX63BJ","identity_key":"carol@initech.dev"}
saved segment "marketers"
2 rows

gtme query --name marketers runs it again. Import a new marketer tomorrow and the segment includes them, while qualified stays what the qualify run decided until a pipeline or a gtme groups edit changes it. gtme groups add marketers-today --from-segment marketers copies a segment's current members into a group.

That's it. That's how groups and segments work.

Why it's this way

An AI judge can answer differently on each run, and a group keeps its first answer. The decision record, ADR-021, made groups separate from segments so that re-judging someone takes a deliberate edit. gtme plan checks that every group a file reads from or gates on exists, and it can't check a SQL query the same way.

The handoff is a write. Nothing runs send when qualify finishes, so each runs on its own timetable (ADR-032).

once: never removes members (ADR-052). The group stays the reviewed set, and removing once: replays it. The cost is that a record that fails every time takes a limit: slot on every run until you fix it. Serve order is oldest-added only.

Where it shows up