block / block/buzz

Webhook body keys named after built-in trigger fields are silently swallowed — {{trigger.text}} always renders empty for webhook triggers

Open
#5,769 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

## Summary

For webhook-triggered workflows, a JSON body key named `text` (or any other built-in trigger field name: `author`, `channel_id`, `timestamp`, `emoji`, `message_id`) can never be read from a template. `{{trigger.text}}` resolves to the empty built-in field instead of the posted value, with no error or warning — the value just vanishes from the rendered message.

## Reproduction

1. Create a webhook-triggered workflow:

```yaml
name: repro
trigger:
on: webhook
steps:
- id: post
action: send_message
text: |
{{trigger.text}}

cc @Someone
```

2. Post to the hook:

```bash
curl -X POST "https:///hooks/" \
-H "X-Webhook-Secret: " -H 'Content-Type: application/json' \
-d '{"text":"hello world"}'
```

3. The relay returns 202 and the run succeeds, but the channel message contains only `cc @Someone` — the `hello world` line is empty.

Renaming the body key to anything non-reserved (e.g. `alert`) and using `{{trigger.alert}}` works, which confirms the payload arrives fine and only the lookup is broken.

## Root cause

The webhook handler puts all body keys into `webhook_fields` and leaves the built-in fields at their defaults (empty strings), [`crates/buzz-relay/src/api/bridge.rs#L1856-L1872`](https://github.com/block/buzz/blob/3afa129ee785cc74d921d0ba969254a8255c4cc0/crates/buzz-relay/src/api/bridge.rs#L1856-L1872):

```rust
// Build trigger context from webhook body fields.
let mut trigger_ctx = buzz_workflow::executor::TriggerContext {
channel_id: ...,
..Default::default() // text, author, … all ""
};
if let Some(Value::Object(ref map)) = body_json {
for (k, v) in map {
...
trigger_ctx.webhook_fields.insert(k.clone(), val_str);
}
}
```

But `TriggerContext::get_field` matches the built-in names *before* consulting `webhook_fields`, [`crates/buzz-workflow/src/executor.rs#L49-L59`](https://github.com/block/buzz/blob/3afa129ee785cc74d921d0ba969254a8255c4cc0/crates/buzz-workflow/src/executor.rs#L49-L59):

```rust
pub fn get_field(&self, name: &str) -> Option<&str> {
match name {
"text" => Some(&self.text), // "" for webhook triggers — shadows the body key
...
other => self.webhook_fields.get(other).map(|s| s.as_str()),
}
}
```

So for webhook triggers the six built-in names shadow same-named body keys, and because `get_field` returns `Some("")` rather than `None`, the template engine's "unknown keys render literally" behavior doesn't kick in either — the value silently disappears.

`text` is an especially unfortunate reserved name: it's the most natural key for the message payload (and the first thing an integrator will try), which makes this an easy foot-gun to hit and a hard one to diagnose — everything returns success end to end.

## Suggested fix

Any of these would do, roughly in order of preference:

1. For webhook triggers, check `webhook_fields` first (or only) — the built-ins other than `channel_id` are meaningless there anyway.
2. Have webhook ingestion populate `trigger_ctx.text` from a body key named `text` (and skip inserting it into `webhook_fields`), so the shadowing becomes harmless.
3. At minimum, reject or warn on reserved-name body keys / document the reserved names in the workflow docs.

Happy to send a patch if maintainers have a preference among these.

---

*Investigated and co-authored with Claude Code (Claude Fable 5).*

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.