Authoring extensions
An extension manifest declares local typed behavior using data, not executable
code. Adding an extension does not require rebuilding birdd.
Read Extensions first for envelope identity, compatibility, installation, and federation semantics.
Complete example
This manifest declares a rainfall envelope, Signal presentation, two Grist columns, a numeric search measure, and a Signal input profile:
manifest_version = 1
extension_id = "org.example.observations/rainfall"
supported_versions = [1]
[signal]
detail_template = """
Rainfall: {{ data.millimeters }} mm
{% if data.condition is defined %}Condition: {{ data.condition }}{% endif %}
"""
receipt_template = "{{ data.millimeters }} mm"
[[projected_fields]]
json_key = "millimeters"
grist_column_label = "Rainfall mm"
grist_column_type = "Int"
editable = "editable"
[[projected_fields]]
json_key = "condition"
grist_column_label = "Condition"
grist_column_type = "Text"
editable = "editable"
[[measures]]
name = "total"
json_key = "millimeters"
aggregate = "sum"
missing = 0
[[profiles]]
name = "rainfall"
summary = "Rainfall observation with a measured depth"
example = "!saw Rain gauge :: rainfall millimeters=12"
[[profiles.fields]]
name = "millimeters"
extension_id = "org.example.observations/rainfall"
extension_version = 1
json_key = "millimeters"
value_type = "positive_u32"
required = true
Save it as extensions/rainfall.toml, add it to the node configuration, and
restart birdd:
[extensions]
manifests = ["extensions/rainfall.toml"]
The profile can then create the extension:
!saw Rain gauge :: rainfall millimeters=12
It produces:
{
"org.example.observations/rainfall": {
"version": 1,
"data": {
"millimeters": 12
}
}
}
The optional condition key can be added through its editable Grist column or
through a complete CLI envelope.
Manifest fields
Manifests are strict TOML. Unknown fields and unsupported values prevent node startup.
| Field | Requirement |
|---|---|
manifest_version | Required; currently 1 |
extension_id | Required valid namespaced envelope ID |
supported_versions | Required non-empty, ascending, unique positive integers |
signal | Optional Signal presentation templates |
projected_fields | Optional list of Grist projections |
measures | Optional list of grouped-search measures |
profiles | Optional list of Signal input profiles |
The manifest ID is both the wire identity and the registry collision boundary. Only one loaded manifest may declare an extension ID.
Signal templates
The optional [signal] table accepts one or both MiniJinja templates:
| Field | Used for |
|---|---|
detail_template | Extension details in !today, !recent, !obs, and !show |
receipt_template | Text appended to Recorded: <subject> after profile-based creation |
Templates receive only this context:
{
"extension": {
"id": "org.example.observations/rainfall",
"version": 1
},
"data": {
"millimeters": 12,
"condition": "steady"
}
}
Access envelope fields through data and metadata through extension:
Rainfall: {{ data.millimeters }} mm
Version: {{ extension.version }}
Undefined values are strict. Guard optional keys explicitly:
{% if data.condition is defined %}Condition: {{ data.condition }}{% endif %}
Birdd compiles templates once when loading the registry. A syntax error, blank
template, template longer than 4096 bytes, or empty [signal] table prevents
startup. Rendering uses a 10,000-instruction fuel limit and recursion limit of
32. Detail output is limited to 1024 bytes per extension and receipt output to
256 bytes.
Signal is plain text, so auto-escaping is disabled. Before insertion, Birdd replaces control characters, collapses whitespace within each non-empty output line, and removes blank lines. A receipt is collapsed to one line. The final message remains subject to Signal’s overall message limit.
Formatting is applied only to versions listed in supported_versions. Missing
templates and unsupported versions fall back to the extension ID in detail
output. A runtime template error, including a missing unguarded key or exhausted
resource limit, is logged and uses the same fallback instead of failing the
Signal command. Receipt errors omit only the receipt text.
Templates are local presentation policy. They are not included in observation events or flock metadata, and a receiving node uses its own installed manifest. See the MiniJinja template syntax for expressions, conditions, loops, and filters.
Projected fields
Each [[projected_fields]] entry maps one top-level extension data key to one
Grist column:
| Field | Accepted values |
|---|---|
json_key | Non-empty top-level key, unique within the extension |
grist_column_label | Non-empty human-readable label |
grist_column_type | Text or Int |
editable | read_only or editable |
Text accepts JSON strings. Int accepts non-negative integers that fit in a
u32. An editable field can be created or corrected from Grist; a read-only
field is projection-only.
Birdd generates a column ID by replacing every non-ASCII-alphanumeric character
in the extension ID and JSON key with _, then joining them with __:
org.example.observations/rainfall + millimeters
-> org_example_observations_rainfall__millimeters
Generated IDs must be unique across the complete loaded registry.
Measures
Each [[measures]] entry exposes a numeric value to grouped Signal search:
| Field | Accepted values |
|---|---|
name | Non-empty name, unique within the extension |
json_key | A projected Int field in the same manifest |
aggregate | Currently sum |
missing | Non-negative contribution when the envelope is absent |
The configuration reference is the extension ID, a ., and the measure name:
org.example.observations/rainfall.total
Use a simple dot-free measure name because references split at the final ..
Malformed fields and unsupported envelope versions contribute nothing; a
missing envelope contributes the configured missing value.
Profiles
Each [[profiles]] entry declares a Signal input profile:
| Field | Requirement |
|---|---|
name | Required, globally unique profile name |
summary | Required non-empty help text |
example | Required non-empty command example |
fields | Required array of profile field declarations |
Each [[profiles.fields]] entry accepts:
| Field | Requirement |
|---|---|
name | Chat-facing name=value key, unique within the profile |
extension_id | An extension loaded in the same registry |
extension_version | A version supported by that extension |
json_key | Non-empty target key in the envelope data |
value_type | Currently positive_u32 |
required | Boolean |
A profile may populate multiple loaded extensions. Birdd groups fields by extension ID and version when constructing envelopes. Declare at least one field; an empty profile has no useful Signal input and can make inference ambiguous.
Use lowercase ASCII token names for profiles and fields. Avoid whitespace and
= because Signal input is whitespace-delimited name=value text. Use names
that distinguish profiles by their required fields when relying on inference.
Version limitations
One manifest can advertise several versions, but projections and measures are shared across all of them. List multiple versions only when their projected keys, types, and measure meanings remain compatible.
Grist creates new envelopes using the first supported version. Since versions must be ascending, this is the lowest listed version. There is no migration or per-version projection mechanism.
Unknown positive versions remain valid opaque envelopes. Typed Signal and Grist behavior ignores versions that are not listed.
Validation boundary
Manifests describe the typed boundaries Birdd currently implements:
- Signal profile input validates
positive_u32values. - Signal templates render locally interpreted envelope data.
- Grist projection and edits validate
Textand unsignedIntvalues. - Measures extract unsigned integers from declared
Intfields.
Manifests are not JSON Schema. They do not validate arbitrary envelopes created through the CLI or received through federation, and they cannot express ranges, cross-field rules, free-form parsers, or executable transformations. Such envelopes remain subject only to core ID, version, and object-shape validation.
Test and deploy
Before enabling a manifest, check:
- Every supported version and malformed typed value
- Generated Grist column IDs and collisions
- Signal profile parsing, inference, and generated envelopes
- Signal detail and receipt rendering, including missing and malformed values
- Measure behavior for values, missing envelopes, malformed fields, and unsupported versions
- Grist projection, creation, editing, and unknown-key preservation
- Restricted audience allowlists and the complete envelope contents
Restart every process that consumes the node configuration. Then inspect and publish the resulting capabilities:
birdd --config birdd.toml grist-schema
birdd --config birdd.toml metadata
birdd --config birdd.toml sync
If projected Grist columns changed, run grist-provision before editors use
them. See Grist integration.