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
- Be a member of 2+ channels (true for basically every real account).
- 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.
- 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.
- Confirm the data really exists: build
buzz-cli and run buzz workflows list --channel <that-channel-id> 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:
// crates/buzz-relay/src/api/bridge.rs
fn extract_channel_from_filter(filter: &nostr::Filter) -> Option<uuid::Uuid> {
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::<uuid::Uuid>().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 <id> (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.
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 viabuzz-clithat the data is present and correctly stored on the relay — this is a read-side bug, not a data-loss bug.Repro steps
Create Workflow→ fill in trigger/steps →Create). The dialog closes with no error, confirming the relay accepted it.buzz-cliand runbuzz workflows list --channel <that-channel-id>authenticated as the same user (or any member). The workflow is returned, full definition intact.Root cause
WorkflowsView.tsxbatches 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-cliland in the same handler —POST /query(crates/buzz-relay/src/api/bridge.rs,query_events/query_events_authed). The catch-all filter path there callsextract_channel_from_filterto resolve which channel a filter is scoped to:This only resolves a scope when the
#htag list has exactly one value. With 2+ values (the batched multi-channel case the Workflows screen relies on), it returnsNoneon every call. Whatever happens downstream of thatNone(broadened access-scope query,d_tag/NIP-33 lookup skipped, or something inbuild_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#hfilters work (proven viabuzz-cli), multi-value#hfilters against kind 30620 don't (proven via the desktop app, which is the only caller using multi-value#hfor this kind).The comment in
WorkflowsView.tsxconfirms this batching was a deliberate, relatively recent optimization ("replaces the per-channel fanout" — previously oneget_channel_workflowscall per member channel viaPromise.all), which is presumably why this wasn't caught earlier — the old per-channel-call pattern would have sent single-value#hfilters and never hit this branch.Suggested fix
Either:
extract_channel_from_filter(and whatever consumes itsNoneresult for kind:30620 specifically) should treat multiple#hvalues as "any of these," matching standard NIP-01 tag-filter semantics, instead of "ambiguous → no scope"; orSecondary, smaller finding along the way
buzz workflows delete --workflow <id>(crates/buzz-relay/src/handlers/side_effects.rs,handle_a_tag_deletion) correctly removes the workflow from the operationalworkflowstable 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, sobuzz 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 adeletecommand to leave the object still listed — worth at least a doc note if it's by design.Environment
buzzrepo,mainbranch, commit35305bf.