Appearance
Process hundreds of records in one routine
Find a list at run time, perform the same bounded workflow for every item, preserve input order, and finish with one aggregate report.
- What you'll build — A read-oriented client-health audit that pulls quiet clients from the Sales Platform, analyzes two aspects of each client in parallel, recommends a next action, and summarizes the entire batch.
- Workflow features —
foreach, nestedparallelandsequencebehavior, item aliases,{previous}, bounded concurrency, ordered results, partial failures, per-step Agents, and an optional batch effect boundary. - Scale — Designed for hundreds of records. The runtime accepts at most 10,000 resolved items and at most 100 simultaneously active Agent leaves.
Build this through Claude Code or Codex using the account-scoped Agents MCP. Start with Build and test a routine with Claude or Codex if the server is not connected yet.
Why foreach is the important part
A normal parallel block has a fixed number of branches written into YAML. foreach gets its list at run time and starts one copy of its nested workflow for each item. The source Agent can obtain that list with any ordinary tool it already has; the YAML does not need a typed client source or a special batch worker.
Each item receives:
{client}— the current item, because this example usesas: client;{item}— the same item under the standard name;{index}— its zero-based position;{previous}— the previous nested step's result for that same client;{input}— the original routine run message.
Separate data paging from Agent work
The source step must collect its input through bounded connector calls. For an exhaustive client sweep, scan_clients returns {clients, count, limit, next_after_id}: request up to 100, pass next_after_id back as after_id, and stop when count < limit. Apply mutable client conditions in the skill. list_clients retains filters and offset for interactive lookup, where a live result is acceptable; it is not a batch snapshot. Do not ask for limit: 0 as a shortcut to "all".
Conversation extraction uses an immutable id cursor because a live chat's activity time can move while you scan it. export_chats returns at most 100 chats with a bounded first or last message window. Feed next_before_id back as before_id on the next call; never calculate an offset or page by the mutable activity timestamp. See the exact contract in Sales tools reference: export_chats. Apply assistant/client/page/channel, activity-window, and operator-state conditions in the skill to each returned row. Those mutable fields intentionally do not define server-side membership across calls.
Use foreach only when each record genuinely needs an Agent decision. A request such as “copy the first ten messages of 7,000 chats to CSV” is deterministic data work: page export_chats, write/checkpoint each page in the caller's skill, then upload the finished file. Creating 7,000 Agent leaves for that copy would be slower and far more expensive without improving the result.
1. Ask Claude or Codex to inspect the account
Use a prompt like this:
text
Build a read-only routine that audits clients with no activity in a window I
provide. Expect 200–500 clients. Use the existing Sales Platform-capable Agent
to return a minimal JSON array with stable client ids, then foreach over it.
For each client, analyze relationship health and data completeness in parallel,
then produce one recommended next action. Do not send messages or update client
records. Summarize successes and failures after the batch.
Inspect my account and the routine documentation first. Validate and show me the
complete canonical YAML before saving. Preview a three-item literal version,
then restore the dynamic source and run readiness. Do not create or enable a
schedule yet.The coding agent should call get_routine_authoring_context, read its Markdown reference, then call get_account_authoring_inventory. It must confirm the actual agent slugs and relevant Sales Platform connector before drafting. The example below uses the standard platform-assistant, personal-assistant, and bulk-text-worker slugs; the coding agent should adapt them if your effective inventory differs.
2. Review the final live YAML
This is the complete routine shape Claude or Codex should produce:
yaml
name: quiet-client-health-audit
display_name: Quiet client health audit
description: Audit quiet clients in parallel and return an ordered action list without changing records.
archetype: personal-assistant
run_message: Find clients with no activity in the last 30 days.
steps:
- title: Find quiet clients
archetype: platform-assistant
prompt: |
Use the connected Sales Platform tools to find clients matching this
window: {input}
Page scan_clients with limit 100. Pass next_after_id back as after_id
until a page count is smaller than its limit. Apply the requested quiet
window to each returned row. Never request an unbounded result.
Return only a JSON array. Keep it compact. Every item must contain a
stable client id, display name, last activity time, current status, and
the minimum recent context needed for an audit. Return [] when none match.
- type: foreach
title: Audit every quiet client
items: "{previous}"
as: client
max_concurrency: 8
steps:
- type: parallel
title: Check health and data quality
steps:
- title: Assess relationship health
archetype: personal-assistant
prompt: |
Assess this client without changing anything: {client}
Return risk level, evidence, and the best next conversation goal.
- title: Check record completeness
archetype: platform-assistant
prompt: |
Inspect the supplied client data only: {client}
List missing or inconsistent fields and do not update the record.
- title: Recommend one next action
archetype: bulk-text-worker
prompt: |
Combine the parallel findings below into compact JSON with client_id,
risk, recommended_action, and reasons:
{previous}
- title: Summarize the batch
archetype: personal-assistant
prompt: |
Summarize this foreach aggregate: {previous}
Report total, succeeded, failed, counts by risk, the ten highest-priority
clients, and the ids of failed items that should be retried. Do not claim
that any client record was changed.The two branches inside each item run at the same time. With max_concurrency: 8, the widest point is 8 items × 2 Agent leaves = 16 active Agent runs, below the routine-wide ceiling of 100. The recommendation for an item waits for both branches and reads their combined output through {previous}.
3. Preview a literal sample
The final items: "{previous}" source is dynamic, so its worst-case call count cannot be known before the source Agent runs. That final routine is valid for live execution but is not eligible for the MCP's bounded preview.
Before saving the final form, have Claude or Codex temporarily replace items with a small representative list:
yaml
items:
- id: sample-001
display_name: Sample retailer
last_activity_at: "2026-07-01T10:00:00Z"
status: qualified
recent_context: Asked for wholesale pricing; no reply for 35 days.
- id: sample-002
display_name: Sample studio
last_activity_at: "2026-06-28T15:30:00Z"
status: new
recent_context: Imported record with no conversation history.
- id: sample-003
display_name: Sample clinic
last_activity_at: "2026-06-20T08:15:00Z"
status: negotiating
recent_context: Proposal sent; decision date is missing.Validate and save that version, approve the billed tool-free preview, and inspect the item and aggregate outputs. Three items produce a statically bounded 11 Agent calls in this graph: one source step, nine item steps, and one summary.
The preview does not query the Sales Platform. Its source step and all connector actions are simulated because AgentOS removes tools. After the graph behaves correctly, ask it to restore items: "{previous}", validate the full YAML, update the saved routine, and call get_routine_readiness again.
4. Run a narrow live read
The Agents MCP cannot start a live run. In MyChatBot, run the final routine with a deliberately narrow window such as “find at most five clients with no activity in 90 days.” Confirm that:
- the source result is a JSON array rather than prose or a Markdown fence;
- every item contains a stable client id;
- no branch attempts a write;
- successes and failures remain aligned with the original item order;
- the final summary names failed ids without inventing results.
Increase the source limit and concurrency gradually. Provider rate limits and model cost usually matter well before the YAML ceiling does.
5. Understand failure and retry behavior
One item failure does not cancel its siblings. The aggregate records total, succeeded, failed, and one ordered row per input item. The batch itself is not automatically retried.
For safe reruns:
- keep stable external record ids in every item;
- have the summary emit the failed ids;
- rerun a list containing only those failures;
- make any eventual write idempotent when the connector supports it.
foreach is not a transaction or an exactly-once job queue. A process can fail after an external write succeeds but before the result is recorded.
Routine recovery can reuse saved items in an interrupted top-level foreach for every account with a supported run. It does not apply retroactively to runs without checkpoints. Saved failures remain failures; recovery continues unfinished work rather than automatically rerunning all failed items.
Optional: turn recommendations into record updates
Once the read-only audit is proven, Claude or Codex can change the final nested step to perform an explicit update through the same Agent's existing tools. Nothing will ask you before it runs, so make this change only after a dry run has shown you the exact list it will touch — the approval below is kept for older specs and does not pause:
yaml
- type: foreach
title: Update every reviewed client
items: "{previous}"
as: client
max_concurrency: 4
approval:
message: Review the client list and approve the described CRM updates.
steps:
- title: Apply one idempotent update
archetype: platform-assistant
prompt: |
Update only the record identified by the stable id in {client}.
Apply the explicitly reviewed fields and return the id plus the tool
result. Do not send a message or change any other record.Routines never pause for approval
A routine that starts runs every step to the end. There is no approval card, no review pause, and no switch that turns one on — the mechanism was removed, not disabled.
approval, approval_message, requires_iteration_review and requires_output_review are still accepted so existing YAML keeps loading and saving, but none of them stops a run. If a routine of yours was written around a gate, treat that step as fully live and re-read its prompt.
So an owner-authored step does whatever its task says, including effects that reach real people — messages, calls, outreach, publication, deletion, live record changes. Writing the step is the authorization; there is no second checkpoint before a customer is contacted. Scope each prompt to exactly the action you want, and rehearse with a dry run (tools switched off) before you attach a schedule or trigger.
(Router requires_user_input is unrelated and still works: it supplies a branch choice the workflow needs, not permission to act.)
Nested foreach, child approvals, Loop iteration review, and Router user/output review are intentionally rejected inside an item. Flatten the list and review the batch as a whole.
Put it on a schedule
After the live read is sound, ask Claude or Codex to call create_routine_schedule with the desired cron and IANA timezone. The MCP creates it disabled. Review normal batch size, calls per item, expected spend, and external rate limits before a separate set_routine_schedule_enabled approval.
See also
- Build routines with Claude or Codex — complete authoring and activation lifecycle
- Routine YAML: foreach — exact item, concurrency and nesting rules
- Adaptive research workflow — the other control-flow nodes in one bounded example
- Usage & billing — every Agent leaf is a billed run
