Closures
Introduction
Section titled “Introduction”Static config is enough until a field must react to the record, tenant, operation, or sibling state. Orbit evaluates callables through almasix.orbit.support.evaluate: if the value is callable, it is invoked with a filtered keyword context (only parameters the callable declares, unless it accepts **kwargs). That is Orbit’s utility injection: declare lambda record: … (or operation, state, user, …) and the host supplies those values even when it also has dozens of other extras.
Deep dive on the helper itself: Support closures. This page is the forms cookbook.
Each pattern below includes explanation, Python, and screenshots of related chrome.
How evaluate filters utilities
Section titled “How evaluate filters utilities”evaluate(candidate, *args, **ctx) tries, in order: candidate(**filtered), candidate(*args), candidate(*args, **filtered), then candidate(). If the callable declares **kwargs, the full context is passed. Otherwise only named parameters present in ctx are injected.
from almasix.orbit.support.evaluate import evaluate
evaluate(lambda record: record['title'], record={'title': 'Hi'}, unused=1)# 'Hi' — unused is ignored because the lambda does not declare itLabel, helper, hint, placeholder
Section titled “Label, helper, hint, placeholder”Almost every copy surface on Component / Field accepts a callable: .label(), .helper_text(), .hint(), .hint_icon(), .placeholder(), .prefix(), .suffix(), and content slots.
TextInput.make('slug') .label(lambda record=None, **_: 'Slug' if record else 'New slug') .helper_text(lambda **ctx: 'Locked' if ctx.get('published') else 'Editable') .placeholder(lambda operation=None, **_: 'generated-after-save' if operation == 'create' else '')

Conditional required
Section titled “Conditional required”.required() accepts a bool or callable. The callable is evaluated in is_required(**ctx) during both render (asterisk, unless overridden by .mark_as_required()) and get_rules().
TextInput.make('slug') .required(lambda data=None, operation=None, **_: ( operation == 'create' or bool((data or {}).get('publish')) ))

Visibility and disabled
Section titled “Visibility and disabled”.visible() / .disabled() take bools or callables. Field also offers .disabled_on(*operations), .hidden_on(*operations), and .visible_on(*operations) for create/edit/view without writing lambdas.
TextInput.make('refund_reason') .visible(lambda record=None, **_: bool(record and record.get('status') == 'refunded'))
TextInput.make('external_id') .disabled_on('edit', 'view') .visible_on('create', 'edit')

Dynamic options
Section titled “Dynamic options”.options() may be a mapping or a callable evaluated at render (and when resolving option groups). Relationship selects use .relationship() instead of hand-rolled options when the related model is known.
from almasix.orbit.forms import Select
Select.make('assignee_id') .label('Assignee') .options(lambda tenant=None, **_: load_assignees(tenant)) .searchable()

Defaults
Section titled “Defaults”.default() stores a static value or callable. Hosts evaluate via get_default(**ctx) when filling empty state.
TextInput.make('priority') .default(lambda **_: 'medium')
Hidden.make('account_id') .default(lambda user=None, **_: getattr(user, 'account_id', None))

After state updated
Section titled “After state updated”.after_state_updated(callback) stores a Python callback for the host to run when state changes. .after_state_updated_js(script) attaches a JS snippet attribute for Alpine-side reactions.
TextInput.make('title') .live() .after_state_updated(lambda state, set_state=None, **_: ( set_state('slug', slugify(state)) if set_state and state else None )) .after_state_updated_js('console.debug($event)')Dehydrate transforms
Section titled “Dehydrate transforms”.trim() and .strip_characters() run inside apply_dehydrate_transforms before .dehydrate_state_using(). Custom dehydrate callbacks receive the (possibly trimmed) value plus context.
TextInput.make('sku') .strip_characters('- ') .trim() .dehydrate_state_using(lambda value, **_: (value or '').upper())

Validation callables
Section titled “Validation callables”Pass callables to .rules(). Validation context includes value, state, field, attribute, operation, and optional unique / exists checkers.
TextInput.make('name') .rules(lambda value, **_: ( True if value and value.lower() != 'admin' else 'That name is reserved.' ))Disable option when
Section titled “Disable option when”Select-family fields support .disable_option_when(callback). The callback receives value, label, state, and render context; returning true disables that <option>.
Select.make('status') .options({'draft': 'Draft', 'paid': 'Paid', 'void': 'Void'}) .disable_option_when(lambda value, record=None, **_: ( value == 'void' and not getattr(record, 'is_manager', False) ))

Repeater item labels
Section titled “Repeater item labels”.item_label() on Repeater/Builder accepts callables with index, item, and state.
Repeater.make('addresses') .schema([TextInput.make('city')]) .item_label(lambda index, item=None, **_: (item or {}).get('city') or f'Address {index + 1}')

View field and content slots
Section titled “View field and content slots”ViewField.content() and Field content slots (above_label, below_content, …) accept callables. Slot/HTML content is not auto-escaped — sanitize untrusted input.
ViewField.make('total') .content(lambda state=None, **_: f'<strong>{state or 0}</strong>')
TextInput.make('notes') .above_content(lambda operation=None, **_: 'Visible on invoices' if operation == 'edit' else '')

Typical utility names
Section titled “Typical utility names”Hosts pass utilities as keyword arguments into render and validate. Declare only the names you need — evaluate drops the rest unless the callable accepts **kwargs. The table below is the usual vocabulary across Orbit resources.
field.render( state.get('slug'), record=post, state=state, operation='edit', user=request.user, tenant=request.tenant, model=Post, resource=self,)
errors = form.validate(state, operation='edit', record=post, user=request.user)| Utility | Common sources |
|---|---|
record |
Resource edit/view fill |
state / data |
Full form state |
value |
Current field value (validation, some callbacks) |
operation |
create / edit / view from Form or ctx |
model / resource |
Relationship resolution |
user / tenant |
Auth / tenancy from host |
index / item |
Repeater item label |
field / attribute |
Validation |
Mental model
Section titled “Mental model”Treat closures as stage directions, not a second schema language. Same field tree; manners that depend on who is in the room. Prefer small lambdas that name their utilities explicitly over grabbing **ctx and digging.
# Prefer:TextInput.make('slug').disabled(lambda record=None, **_: bool(record and record.get('published')))
# Avoid opaque catch-alls unless you truly need every utility:TextInput.make('slug').disabled(lambda **ctx: bool((ctx.get('record') or {}).get('published')))See also Validation for callable rules and Support closures for the evaluate helper itself.
For building new field types see Custom fields.