Skip to content

Bindings

Bindings connect parent-level property values to child component properties. They are stored on each ComponentPlacement and resolved at compile time, producing the HashMap<String, PropertyValue> that Component::compile() receives.

crates/core/src/template/placement.rs [30:35]
pub enum PropertyBinding {
Reference {
property_id: String, // Key of a parent template property
},
Static {
value: PropertyValue, // Hardcoded value
},
}

Points to a parent template’s property by key. When the parent’s value changes, the child sees the updated value on the next compile cycle.

// "Use the parent's 'topic' property as this component's 'title'"
PropertyBinding::Reference {
property_id: "topic".to_string(),
}

A hardcoded value that doesn’t depend on the parent. Useful for configuration that should stay fixed regardless of template property changes.

// "Always use blue background"
PropertyBinding::Static {
value: PropertyValue::String("#0066CC".to_string()),
}

Bindings are stored on ComponentPlacement<P>, which represents a positioned component within a modality:

crates/core/src/template/placement.rs [10:17]
pub struct ComponentPlacement<P> {
pub id: String,
pub component_key: String,
pub component_id: Option<String>,
pub position: P,
pub bindings: HashMap<String, PropertyBinding>, // component prop key → binding
}

The bindings map uses the child component’s property key as the map key, and the PropertyBinding specifies where the value comes from.

A whiteboard has a template property "prompt". A TextBox component has a property "text". The binding connects them:

ComponentPlacement {
id: "textbox-1".to_string(),
component_key: "text_box".to_string(),
position: Rect { x: 10.0, y: 20.0, width: 400.0, height: 200.0 },
bindings: HashMap::from([
// Child's "text" ← Parent's "prompt"
("text".to_string(), PropertyBinding::Reference {
property_id: "prompt".to_string(),
}),
// Child's "backgroundColor" ← always white
("backgroundColor".to_string(), PropertyBinding::Static {
value: PropertyValue::String("#FFFFFF".to_string()),
}),
]),
}

resolve_bindings() is a free function that converts bindings into concrete values using the parent’s property values:

crates/core/src/template/placement.rs [107:124]
pub fn resolve_bindings(
bindings: &HashMap<String, PropertyBinding>,
parent_values: &HashMap<String, PropertyValue>,
) -> HashMap<String, PropertyValue> {
bindings.iter().map(|(key, binding)| {
let value = match binding {
PropertyBinding::Static { value } => value.clone(),
PropertyBinding::Reference { property_id } =>
parent_values.get(property_id)
.cloned()
.unwrap_or(PropertyValue::Null),
};
(key.clone(), value)
}).collect()
}
  • Reference looks up the parent’s value by property_id. If the parent doesn’t have that property, the result is Null.
  • Static returns the hardcoded value directly.

Bindings are resolved during Modality::layout(), which is called by Modality::compile():

flowchart LR
TV[Template Values] --> RB[resolve_bindings]
B[Placement Bindings] --> RB
RB --> CV[Component Values]
CV --> CC["Component::compile()"]
M[Metadata] --> CC
CC --> O[Output Elements]

The default layout() implementation iterates placements and resolves bindings for each:

// Inside Modality::layout() default implementation:
for p in self.placements() {
let meta = self.child_metadata(p);
let resolved = resolve_bindings(&p.bindings, values);
compile_child(p, &meta, &resolved).map(|out| (p.clone(), out))
}

During AI generation, the orchestrator resolves bindings before the child subagent runs. The child sees bound values as its own properties — no awareness of the parent is needed. This is part of the “context flows down, never up” principle described in the orchestrator.

// Property bindings resolve parent values into child properties
// before the child's subagent starts
let resolved = resolve_bindings(&placement.bindings, &parent_values);
// Child subagent sees these as its own property values