Reading Connected Agents in Rhai
IntermediateUse get_connected, sum_prop, and count_prop to let agents read each other's state through the graph — the way Metapad agents coordinate.
In Metapad, agents coordinate through the graph itself — they read each other's state through their links. There's no message-passing, no event system; just direct reads. This keeps every interaction visible in the model, and it makes simulations easy to debug because everything important is on the diagram.
get_connected — the basic traversal
let teammates = get_connected(agent, "belongs_to");
get_connected returns an array of agents reachable from the current agent via a given relationship type. It walks the link in either direction, so the same call works whether your agent is the source or the target of the relationship. Once you have an array, you can do anything Rhai's array methods support:
teammates.len() // count
teammates.filter(|a| a.get_prop("role") == "Eng") // subset
sum_prop and count_prop — common aggregations
For the two most common patterns — summing a numeric property across the array, or counting agents with a particular value — Metapad provides shortcuts:
sum_prop(get_connected(agent, "has_member"), "salary")
count_prop(get_connected(agent, "has_member"), "state", "Active")
These are exactly what you'd want for KPI rollups, headcount, total cost, total revenue — anywhere a parent agent needs to summarize its children.
Binding the traversal to a variable and aggregating it twice reads better than repeating the call, and costs nothing extra:
let members = get_connected(agent, "has_member");
sum_prop(members, "salary") / count_prop(members, "state", "Active")
When one node type serves different kinds of instance
Formulas live on the node type, so every instance of that type runs the same formula — even when the instances are connected differently. A reporting type might have per-item agents that read their item via documents, plus one roll-up agent that reads all of them via aggregates.
Asking for a relationship an agent doesn't have is not an error: get_connected returns an empty array, sum_prop over it is 0, and no diagnostic is raised. So the simplest formula just covers both cases and lets the absent side contribute nothing:
sum_prop(get_connected(agent, "documents"), "volume")
+ sum_prop(get_connected(agent, "aggregates"), "volume")
(A relationship name that matches no type in your metamodel is reported — that one is a typo, not a legitimately absent link.)
When the two cases need genuinely different logic rather than a sum, branch on has_relationship:
if has_relationship(agent, "aggregates") {
sum_prop(get_connected(agent, "aggregates"), "volume") * agent.get_prop("roll_up_factor")
} else {
sum_prop(get_connected(agent, "documents"), "volume")
}
And when only the name differs, lift it into a variable instead of duplicating the body — as long as every value you assign is a plain string, Metapad still resolves the dependency:
let rel = "documents";
if agent.get_prop("is_aggregate") { rel = "aggregates"; }
sum_prop(get_connected(agent, rel), "volume")
count_connected(agent, "rel") gives the number of links, for the "only roll up when there are at least N" cases.
When do connected reads happen? (T, not T−1)
A formula without an offset reads its neighbours' values at the current timestep. Metapad works out the dependencies between all computed properties and evaluates them in order, so by the time a team's personnel_costs runs, its members' salary for this step has already been computed.
// The members' salary AT THIS timestep
sum_prop(get_connected(agent, "has_member"), "salary")
The engine only falls back to the previous step's value where that ordering is impossible — i.e. where the properties genuinely form a cycle. So you do not need -1 offsets to "break cycles that might exist": add an offset when your model has a real delay, and let the ordering do its job otherwise. (Adding offsets defensively is a common way to inject a step — or a whole simulated year — of delay that isn't in the real system.)
Act methods are the exception, and deliberately so: because every set_prop write is applied only after all agents have run, a script that reads another agent's act-written property sees the previous step's value. See Writing Act Methods.
Reading the past
Some processes have natural time lags — a factory ships today, the warehouse receives tomorrow. The 3-argument form of get_connected lets a formula see what its neighbors looked like one or more timesteps ago:
// What the connected factories shipped one tick ago
sum_prop(get_connected(agent, "supplied_by", -1), "shipped")
This is the famous Beergame pattern: warehouse demand at time T pulls from factory shipments at T−1, modeling delivery time as a structural feature of the model rather than something you have to remember to encode.
Offset rules — and which forms are fast
An offset must be a finite number ≤ 0 (floats round to the nearest whole step, so -9.7 means ten steps back). An infinite, NaN, or positive offset returns nothing and raises a diagnostic rather than guessing — a lag parameter that flips sign or divides by zero should announce itself, not quietly hand you a plausible wrong series.
The offset can be an expression, which is how you drive a delay from a slider or a per-agent property. Two forms stay on the fast path, where Metapad fetches exactly the one past value each cell needs:
sum_prop(get_connected(agent, "supplied_by", -2), "shipped") // literal
sum_prop(get_connected(agent, "supplied_by", agent.get_prop("delay")), "shipped") // a property
(Both also work through a let binding.) Any other expression — arithmetic on a property, a conditional, a computed local — still produces the right answer, but the engine can no longer tell in advance which past values are needed, so it builds the agent's entire history for every cell. You'll see an "unresolved dynamic references" warning in the Diagnostics tab, and the cost grows with the square of the number of timesteps, which is very noticeable on a long run:
// Slow: the offset is arithmetic, so history can't be resolved ahead of time
get_connected(agent, "documents", -(agent.get_prop("lag_months") / 12.0))
The fix is mechanical: compute the offset into its own property (lag_steps, a formula of its own) and pass that property.
Offsets compose when you read a single agent out of a shifted array: get_connected(agent, "r", -1) hands you neighbours as they were at T−1, so m.get_prop("x", -1) on one of them reads T−2.
A worked example: limits to growth
Imagine a population whose growth is limited by a finite carrying capacity. The intro model has this exact setup. The growth factor is adjusted by a connected Resource Adequacy node:
// Growth rate adjusted by connected resource adequacy
let resources = get_connected(agent, "limit_growth");
let resource_factor = if resources.len() > 0 {
resources[0].get_prop("value")
} else {
1.0
};
agent.get_prop("growth_factor") * resource_factor
The same formula works whether there's one resource node or several — and resource depletion automatically slows growth without anyone wiring up an explicit "resource depleted" event.
Tips
- Keep the graph honest. If two agents need to coordinate, draw the link. Hidden coordination through naming conventions or external state is much harder to reason about.
- Use aggregations for aggregations.
sum_propandcount_propare clearer than a manualforloop when you're totalling or counting. But reach for a loop or an index when that's what you mean — bothfor m in get_connected(agent, "r")andresources[0].get_prop("value")are resolved just as efficiently, as long as the array comes fromget_connected. - Use offsets for genuine delays, not to paper over circular dependencies — and keep them in one of the two fast forms above.
Next steps
- Writing Property Formulas — where these reads typically live
- Writing Act Methods — when the read drives a multi-property update
- Diagnosing Simulation Errors and Warnings — when reads return surprising values
- Rhai Function Reference — full signatures for every helper
Try it live
Open these working examples in the app and explore them yourself.
This content was written collaboratively with AI.