Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

FieldRequirement
manifest_versionRequired; currently 1
extension_idRequired valid namespaced envelope ID
supported_versionsRequired non-empty, ascending, unique positive integers
signalOptional Signal presentation templates
projected_fieldsOptional list of Grist projections
measuresOptional list of grouped-search measures
profilesOptional 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:

FieldUsed for
detail_templateExtension details in !today, !recent, !obs, and !show
receipt_templateText 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:

FieldAccepted values
json_keyNon-empty top-level key, unique within the extension
grist_column_labelNon-empty human-readable label
grist_column_typeText or Int
editableread_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:

FieldAccepted values
nameNon-empty name, unique within the extension
json_keyA projected Int field in the same manifest
aggregateCurrently sum
missingNon-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:

FieldRequirement
nameRequired, globally unique profile name
summaryRequired non-empty help text
exampleRequired non-empty command example
fieldsRequired array of profile field declarations

Each [[profiles.fields]] entry accepts:

FieldRequirement
nameChat-facing name=value key, unique within the profile
extension_idAn extension loaded in the same registry
extension_versionA version supported by that extension
json_keyNon-empty target key in the envelope data
value_typeCurrently positive_u32
requiredBoolean

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_u32 values.
  • Signal templates render locally interpreted envelope data.
  • Grist projection and edits validate Text and unsigned Int values.
  • Measures extract unsigned integers from declared Int fields.

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.