Summary
TriggerDef::Schedule gives workflows a real cron/interval scheduler, but ActionDef
(crates/buzz-workflow/src/schema.rs:92) can only communicate:
SendMessage · SendDm · SetChannelTopic · AddReaction · CallWebhook · RequestApproval · Delay
There is no way for a scheduled workflow to cause work to happen on a machine. In practice
that means the scheduler can't be used for the most common reason people want a scheduler —
running a job — without building a separate HTTP surface to receive the trigger.
I'd like an action along the lines of RunCommand: a workflow step that invokes a
pre-registered, allowlisted command on a host, rather than an arbitrary shell string.
Why CallWebhook doesn't cover it
It's the documented workaround, and it works, but the cost is disproportionate for a local job:
check_ssrf rejects private and reserved addresses
(crates/buzz-workflow/src/executor.rs:745, :766). That's correct and I'm not asking
for it to be relaxed — but it means a workflow cannot reach a service on the same host
or LAN as the relay. The only path is out through the public internet and back.
- So triggering a local job requires exposing a new authenticated HTTPS endpoint to the
internet. For a nightly cache refresh on a box that already sits next to the relay, that
is a real security surface added purely to receive a timer tick.
- Every user who wants a scheduled job writes the same receiver: auth check, ACK, detach,
report. That's a missing primitive, expressed as boilerplate.
Proposed shape
Deliberately not arbitrary shell — the value is in a narrow interface:
trigger:
schedule:
cron: "30 10 * * *"
actions:
- run_command:
command: decksmith-sync # id of a registered command, NOT a shell string
args: ["--store", "all", "--full"]
timeout: 20m
capture_output: true
- send_message:
text: "Inventory sync: {{ steps.0.status }} — {{ steps.0.stdout_tail }}"
Sketch of the properties that seem to matter:
- Registration is out-of-band and host-side — an operator declares
decksmith-sync → /usr/local/bin/decksmith-sync in host config. The workflow can only
name a registered id; it can never introduce a new executable.
- Args allowlisted or schema-constrained per command, so a workflow can't smuggle
behavior through argv.
- Runs under the existing systemd sandbox, same posture as agent units
(NoNewPrivileges, ProtectSystem=strict, dedicated service user, declared
ReadWritePaths). The unit stays the authority boundary.
- Long jobs shouldn't block the executor — either async with completion posted as a
follow-up event, or a documented timeout ceiling.
- Output redaction — and per the lesson in this codebase's own broker work, error text
must not echo caller-supplied values, or redaction becomes a confirmation oracle.
- Presumably gated by channel role, and worth considering whether it should be
owner-only by default given it is the most powerful action in the set.
Use case that motivated this
A nightly inventory sync (~11 min) on the same host as the relay. Everything else about it
is a natural fit for Buzz: the schedule, reporting results into a channel, alerting on
failure, and an agent in that channel answering questions about the data afterward.
The only missing piece is "start the job", and closing that one gap currently costs a
public HTTPS endpoint. With RunCommand the entire job is one workflow definition and no
new attack surface.
Other shapes this would cover: backups, mirror/reconciler ticks, health probes that need
a local binary, cache warms, report generation.
Alternatives considered
| Alternative |
Why it falls short |
CallWebhook → local receiver |
Works today; costs a public endpoint + per-user boilerplate (above) |
Schedule → SendMessage → agent runs it via MCP shell |
Puts an LLM in the path of a deterministic job; long tool calls are fragile |
| Keep cron/systemd, use Buzz only for reporting |
What most people will do — but then the scheduler in Buzz goes unused for its main purpose |
Happy to help shape or implement this if the direction is welcome.
Verified against a local checkout at 44599e7. Line references are from that tree.
Summary
TriggerDef::Schedulegives workflows a real cron/interval scheduler, butActionDef(
crates/buzz-workflow/src/schema.rs:92) can only communicate:There is no way for a scheduled workflow to cause work to happen on a machine. In practice
that means the scheduler can't be used for the most common reason people want a scheduler —
running a job — without building a separate HTTP surface to receive the trigger.
I'd like an action along the lines of
RunCommand: a workflow step that invokes apre-registered, allowlisted command on a host, rather than an arbitrary shell string.
Why
CallWebhookdoesn't cover itIt's the documented workaround, and it works, but the cost is disproportionate for a local job:
check_ssrfrejects private and reserved addresses(
crates/buzz-workflow/src/executor.rs:745,:766). That's correct and I'm not askingfor it to be relaxed — but it means a workflow cannot reach a service on the same host
or LAN as the relay. The only path is out through the public internet and back.
internet. For a nightly cache refresh on a box that already sits next to the relay, that
is a real security surface added purely to receive a timer tick.
report. That's a missing primitive, expressed as boilerplate.
Proposed shape
Deliberately not arbitrary shell — the value is in a narrow interface:
Sketch of the properties that seem to matter:
decksmith-sync → /usr/local/bin/decksmith-syncin host config. The workflow can onlyname a registered id; it can never introduce a new executable.
behavior through argv.
(
NoNewPrivileges,ProtectSystem=strict, dedicated service user, declaredReadWritePaths). The unit stays the authority boundary.follow-up event, or a documented timeout ceiling.
must not echo caller-supplied values, or redaction becomes a confirmation oracle.
owner-only by default given it is the most powerful action in the set.
Use case that motivated this
A nightly inventory sync (~11 min) on the same host as the relay. Everything else about it
is a natural fit for Buzz: the schedule, reporting results into a channel, alerting on
failure, and an agent in that channel answering questions about the data afterward.
The only missing piece is "start the job", and closing that one gap currently costs a
public HTTPS endpoint. With
RunCommandthe entire job is one workflow definition and nonew attack surface.
Other shapes this would cover: backups, mirror/reconciler ticks, health probes that need
a local binary, cache warms, report generation.
Alternatives considered
CallWebhook→ local receiverSendMessage→ agent runs it via MCP shellHappy to help shape or implement this if the direction is welcome.
Verified against a local checkout at
44599e7. Line references are from that tree.