Skip to content

Call Baxter architecture

Call Baxter is a generic event-driven callback runtime. Its organizing flow is:

external fact
    |
    v
Signal / EventEnvelope
    |
    v
RuleSpec match + policy
    |
    v
ActionInvocation
    |
    +--> on_success / on_error follow-ups
    |
    v
external or local effect

Telegram is one interface to that flow, not the definition of the system. A signal can come from Telegram, HTTP polling, MQTT, CalDAV, Docker state, a webhook, or another plugin. An action can send a Telegram message, write a file, publish to WordPress or a social network, make an HTTP request, or invoke any other registered executor.

Four primitives

Signal — EventEnvelope

A signal is a normalized fact. It has a source, an event kind, an occurrence time, optional subject/trace references, and an attrs object carrying source-specific data. Plugins declare the context fields their event kinds yield, so rules and downstream actions have an explicit data contract.

Examples include telegram.message.new, http.check.ok, mqtt.message.received, and caldav.event.upcoming.

Rule — RuleSpec

A rule is declarative YAML that decides which actions should run for a matching signal. Rules can match source, kind and attributes; render action arguments from event context; define variables; apply priority/conflict/rate-limit policy; and select sequential or parallel execution.

Rules are configuration. They do not contain the side-effect implementation itself.

Action — ActionInvocation

An action names a registered action kind plus structured arguments. The runtime validates the action's expects contract before execution, records the action and result, and can recursively run on_success or on_error follow-ups.

The runtime deliberately allows arbitrary plugin actions. That makes the scaffold useful, but it also means replay safety cannot be inferred from an action name. See Arbitrary actions and replay safety.

Schedule — ScheduleSpec

A schedule is a persisted periodic trigger for a plugin poller. A due schedule claims a renewable SQLite execution lease, invokes the poller, and the poller publishes zero or more ordinary EventEnvelope signals back through the same runtime. Schedules therefore create signals; they do not bypass the signal → rule → action model.

ScheduleSpec --> poller --> EventEnvelope --> RuleSpec --> ActionInvocation

Runtime boundaries

The core runtime owns normalization, validation, rule evaluation, durable audit state, schedule leases, action-chain execution, replay evidence, and daemon/CLI control surfaces. Interface plugins own protocol-specific I/O and translate between external systems and those core contracts.

That separation gives Call Baxter three useful layers:

Layer Responsibility Examples
Core signal/rule/action execution and durable policy runtime, daemon, CLI, registry, replay evidence, schedule leases
Interfaces produce signals and/or execute actions against outside systems Telegram, WordPress, Bluesky, Mastodon, X, HTTP, MQTT, CalDAV
Deployment/examples assemble concrete plugins, schedules and rule YAML Docker, Compose, Ansible, Telegram→WordPress, social mirroring

A complete flow

id: report-health-failure
enabled: true
match:
  source: http
  kind: http.check.failed
actions:
  - kind: telegram.send_message
    args:
      chat_id: 123456
      text: "Health check failed: {{ event.attrs.url }}"
    on_error:
      - kind: file.out
        args:
          path: "failed-notifications.log"
          content: "{{ event.attrs.url }}\n"
          mode: append

The important architecture is not “HTTP talks to Telegram.” It is that an HTTP plugin emits a typed signal, a rule selects an action, and the registered Telegram executor performs the effect. The fallback is another action with its own safety properties.

Where to go next