Workflows list returns empty for any account with more than one channel
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Workflows list returns empty for any account with more than one channel
### Summary
The desktop app's Workflows screen (`/workflows`) always shows "No workflows yet," even for channels that genuinely have workflow definitions saved. Confirmed via `buzz-cli` that the data is present and correctly stored on the relay — this is a read-side bug, not a data-loss bug.
### Repro steps
1. Be a member of 2+ channels (true for basically every real account).
2. Create a workflow in any channel via the desktop app's Workflows screen (`Create Workflow` → fill in trigger/steps → `Create`). The dialog closes with no error, confirming the relay accepted it.
3. Go to the Workflows overview screen. It shows "No workflows yet," including after clicking the manual refresh control and after a full app quit/relaunch.
4. Confirm the data really exists: build `buzz-cli` and run `buzz workflows list --channel ` authenticated as the same user (or any member). The workflow is returned, full definition intact.
### Root cause
`WorkflowsView.tsx` batches its list query across every channel the user belongs to in a single relay filter, rather than querying per-channel (`desktop/src/features/workflows/ui/WorkflowsView.tsx`, `getChannelsWorkflows(channelIds)` → one `{"kinds": [30620], "#h": [id1, id2, ...]}` filter with N channel ids).
On the relay side, both the desktop app and `buzz-cli` land in the same handler — `POST /query` (`crates/buzz-relay/src/api/bridge.rs`, `query_events` / `query_events_authed`). The catch-all filter path there calls `extract_channel_from_filter` to resolve which channel a filter is scoped to:
```rust
// crates/buzz-relay/src/api/bridge.rs
fn extract_channel_from_filter(filter: &nostr::Filter) -> Option {
let h_tag = nostr::SingleLetterTag::lowercase(nostr::Alphabet::H);
filter.generic_tags.get(&h_tag).and_then(|vs| {
if vs.len() == 1 {
vs.iter().next()?.parse::().ok()
} else {
None
}
})
}
```
This only resolves a scope when the `#h` tag list has **exactly one** value. With 2+ values (the batched multi-channel case the Workflows screen relies on), it returns `None` on every call. Whatever happens downstream of that `None` (broadened access-scope query, `d_tag`/NIP-33 lookup skipped, or something in `build_event_query_from_filter` / the DB layer) ends up excluding kind:30620 results specifically — I didn't trace the exact final SQL step, but the failure is 100% reproducible and starts exactly here: single-value `#h` filters work (proven via `buzz-cli`), multi-value `#h` filters against kind 30620 don't (proven via the desktop app, which is the only caller using multi-value `#h` for this kind).
The comment in `WorkflowsView.tsx` confirms this batching was a deliberate, relatively recent optimization ("replaces the per-channel fanout" — previously one `get_channel_workflows` call per member channel via `Promise.all`), which is presumably why this wasn't caught earlier — the old per-channel-call pattern would have sent single-value `#h` filters and never hit this branch.
### Suggested fix
Either:
- `extract_channel_from_filter` (and whatever consumes its `None` result for kind:30620 specifically) should treat multiple `#h` values as "any of these," matching standard NIP-01 tag-filter semantics, instead of "ambiguous → no scope"; or
- The desktop's batched query should stay batched for scope purposes but not depend on this single-channel-only resolution path for kind:30620.
### Secondary, smaller finding along the way
`buzz workflows delete --workflow ` (`crates/buzz-relay/src/handlers/side_effects.rs`, `handle_a_tag_deletion`) correctly removes the workflow from the operational `workflows` table and invalidates the engine's cache — so a deleted workflow genuinely stops being live/executable. But it doesn't touch the underlying raw kind:30620 event, so `buzz workflows list`/`get` (and presumably the same desktop code path, once the bug above is fixed) will keep showing the "deleted" workflow as a stale historical event indefinitely. Might be intentional (append-only audit trail), but it's surprising for a `delete` command to leave the object still listed — worth at least a doc note if it's by design.
### Environment
- `buzz` repo, `main` branch, commit `35305bf`.
- Reproduced against a self-hosted relay (Docker Compose), not buzz.dev's hosted relay — but the bug is in shared relay code, not deployment-specific.
Contributor guide
Assessment
This issue has not been assessed yet.