Teams as one definition.
A multi-agent workflow in loomcycle used to need a half-dozen separate agent definitions registered globally. Plus the skills those agents called. Plus the channels they published to. Plus the schedule or webhook that kicked the whole thing off. The team definition itself was a state machine that pointed at the agents by name, and the agents were someone else's problem to deploy and keep in sync.
A team shipped between environments broke whenever a referenced agent did. A team shipped to a new tenant required a dozen pre-registrations. A team meant to run in a schedule required a schedule definition in a different file that called the team definition in yet another file. Nothing was wrong with any of these pieces individually. The problem was that they were pieces.
v1.103 through v1.108 collapses the pieces into one file.
The short version. v1.103 added template-and-capture between team states: one state's output flows into the next state's template; capture the plain-text answer into a variable without the sub-agent header. v1.104 made a team definition carry its own local agents, skills, channels, schedules, webhooks, plus declared variables a start sets. Schedules and webhooks can start a team directly. v1.105 added TeamDef op=verify: check a team before saving it; returns severity: refused | unrunnable | advisory with JSON paths at fault. v1.108 opened operator access to a team's own local channels so an operator can watch a running walk without widening the channel's visibility to every agent in the tenant.
Template-and-capture
A team state is a sub-agent run. Before this arc, the state's output arrived at the next state as a glued string: a header line naming the sub-agent plus the final text. If the next state wanted just the answer, it had to parse the header. If the next state wanted to construct a prompt that referenced the previous state's answer, it had to interpolate manually.
v1.103 adds two primitives.
Template: the next state's prompt can reference the previous state's output by name, with the sub-agent header stripped. ${steps.triage.answer} expands to the plain-text answer of the state named triage. No parsing. No header.
Capture: a state can declare that its answer should be captured into a named variable. capture: {as: "priority"} stores the state's answer under $priority for every later state to reference. The variable persists through the whole walk.
states:
triage:
agent: priority-decider
prompt: "Rate this ticket's urgency: ${input.body}"
capture:
as: priority
dispatch:
agent: router
prompt: "Priority is ${priority}. Ticket: ${steps.triage.answer}"
No manual header stripping. No separate orchestration code to pass values between agents.
A team carries its own agents, skills, channels, schedules, webhooks
This is the big change. Before v1.104, a team definition could only reference globally-registered agents by name. After v1.104, a team definition can carry its own:
local.agents: agent definitions that only exist inside this team.local.skills: skills the local agents can use.local.channels: channels the states publish to and read from.local.schedules: cron entries that fire this team on a schedule.local.webhooks: webhook endpoints that fire this team on an inbound request.
Locals are addressed by their local name inside the team file, and resolve against the team's own definitions first. A local priority-decider in team support-triage does not collide with a global priority-decider; they live in different namespaces.
local:
agents:
priority-decider:
system_prompt: |
You rate ticket urgency on a 0-4 scale.
Output only the integer.
tools: []
model: chat/local-small
schedules:
hourly-triage:
cron: "0 * * * *"
start_with:
input: "$pending_queue"
The team file ships as one object. Deploying it registers the team definition and all its locals under one tenant transaction. Removing it removes all its locals. The locals cannot be referenced from outside the team, which is both a scope boundary and a deployment convenience.
Declared variables. A team can declare the variables a start is expected to set: variables: [input, pending_queue]. A start that forgets one gets a clean error with the missing name. A start that sends an unexpected variable gets a clean error with the extra name. The variables are typed (string, int, bool, object) and the type-check runs at start time, not three states into the walk.
Start a team from a schedule or a webhook
With local schedules and webhooks in the team definition, a team can be started two more ways:
Schedule: a cron entry in local.schedules fires the team at the named cadence. The schedule carries the start_with payload the team expects. If the schedule's variables do not match the team's declared variables, config load fails.
Webhook: a webhook entry in local.webhooks gives the team an inbound HTTP endpoint. The webhook validates its signature (HMAC against a tenant secret), parses the body into the team's declared variables, and starts a walk.
Both routes go through the same TeamDef op=run internally, so a team that works manually works on schedule and on webhook. One shape, three triggers.
Verify before saving
TeamDef op=verify (v1.105) checks a team definition without committing it. The response is one of three severities:
refused: the definition is invalid. Config load would fail. The response names the JSON path at fault (local.agents.priority-decider.system_prompt) and the specific problem.unrunnable: the definition is valid, but a walk cannot start. Missing a required variable, references a non-existent channel, has an unreachable state. Save is allowed; starting a walk would error.advisory: the definition works, but something looks off. A state that is reachable but never terminates in the state graph. A declared variable that no state references. Save is allowed; the warning sits on the record.
The op is callable from HTTP, gRPC, MCP, TS, Python. The canvas Check button calls it (see the canvas post) and surfaces the findings next to the JSON cursor.
Operator view into team-local channels
v1.108 shipped the last piece: an operator can watch a team's local channels from outside the team. Before, a channel declared in local.channels was invisible to the operator console; the only way to see traffic was to be an agent inside the team. That meant debugging a stuck walk required reading the walk's log by hand instead of subscribing to the channel it was waiting on.
The operator-visibility is read-only and scoped to the operator's own tenant. It does not widen the channel to any other agent. An agent that was not granted the channel still cannot read it; only the operator identity, with the operator token, can.
What this does not fix
Three limits worth naming.
Team names with more than one segment cannot declare locals. A team named support/triage is a valid team name, but the local-agents feature requires a single-segment name. The constraint is temporary (the name is used as a key in the local-resolution namespace and the slash would collide with the resolver's own path syntax). Workaround: rename to support-triage for now.
Local names cap at 64 characters, pattern [A-Za-z0-9_-]. Longer names, or names with dots or slashes, are refused at config load. The limit matches what the resolver indexes efficiently.
Team walks do not migrate across replicas. A walk is bound to the replica that started it. If that replica restarts, the walk ends with status failed. Children spawned by the walk survive (per the detached sub-agents post); the walk's own state machine does not. A restart-safe team walk is on the roadmap and will share plumbing with the configured-runs work.
What this makes possible
A workflow that used to need six separate definitions (team, four agents, one schedule) now ships as one file. A tenant onboarding gets the whole workflow in one deploy. A test harness can spin up the whole thing in one call and tear it down in one.
The visible payoff is the loomboard canvas: because the team file is self-contained, the canvas can save it, check it, download it, import it elsewhere, and run it, without the operator having to pre-register anything else. The canvas and the one-file team arrived in the same arc because they are the same work from two sides.
What comes next
The remaining team-side gap is the walk-migrates-across-replicas work above. On the agent side, detached sub-agents and the Decision tool are the primitives that fill out the "a team is a graph of ways to make a decision" picture. Then the visual canvas you build those graphs on.