fix(flows): resolve a non-owner Slack DM recipient via a lookup node (#4955)

This commit is contained in:
Cyrus Gray
2026-07-16 17:06:45 +05:30
committed by GitHub
parent 3d76d12916
commit d09e8c1e5f
2 changed files with 104 additions and 0 deletions
@@ -400,6 +400,68 @@ mod tests {
ask the user for their member id in one question rather than \
guessing a channel"
);
// Positive: non-owner DM resolution — the prompt must teach the
// builder to resolve a NAMED recipient who is NOT the connected
// owner via a lookup node, not just the owner's own
// `platform_user_id`. This guidance must be PLATFORM-AGNOSTIC (no
// toolkit-specific slug hardcoded) — the same shape applies to
// Slack, Discord, Telegram, or any other messaging toolkit.
assert!(
STANDING_PROMPT.contains("is NOT the connected"),
"standing prompt must teach the non-owner DM case explicitly"
);
assert!(
STANDING_PROMPT.contains("platform-agnostic"),
"standing prompt must state the non-owner DM guidance is \
platform-agnostic, not tied to one toolkit"
);
assert!(
STANDING_PROMPT.contains("search_tool_catalog { query, toolkit }"),
"standing prompt must teach resolving the lookup action via \
search_tool_catalog scoped to the TARGET toolkit, rather than \
hardcoding one platform's slug"
);
assert!(
STANDING_PROMPT.contains("tool_call` node upstream of the send"),
"standing prompt must teach wiring the lookup as a tool_call \
node upstream of the send node"
);
assert!(
STANDING_PROMPT.contains("resolves to exactly one match"),
"standing prompt must require a name search to resolve to \
exactly one match before binding it without asking"
);
assert!(
STANDING_PROMPT.contains("ask the user to confirm which person"),
"standing prompt must preserve the safety rule: never message an \
unverified same-name match, ask instead when ambiguous"
);
assert!(
STANDING_PROMPT.contains("Check the send action")
&& STANDING_PROMPT.contains("open conversation"),
"standing prompt must teach checking the send tool's own contract \
for a required open-conversation step, handled generally via the \
contract rather than a single-platform special case"
);
// Negative: none of the non-owner DM guidance may hardcode a
// toolkit-specific action slug or arg name — the reviewer flagged an
// earlier draft of this guidance as Slack-only, which violates the
// platform-agnostic rule.
for banned in [
"SLACK_FIND_USERS",
"SLACK_LIST_ALL_USERS",
"config.args.email",
"exact_match",
] {
assert!(
!STANDING_PROMPT.contains(banned),
"standing prompt's non-owner DM guidance must not hardcode \
the platform-specific `{banned}` — it must stay \
platform-agnostic (any messaging toolkit)"
);
}
}
#[test]
@@ -623,6 +623,48 @@ into exactly one bucket before you write the node:
`#channel` name) — no need to ask. Only if `platform_user_id` is null
for that connection, ask the user for their member id in ONE concise
question rather than guessing a channel.
- "DM `<name>`" / "message `<name>`" where `<name>` is NOT the connected
owner (no matching `platform_user_id`) → you don't have their platform
user id up front, and guessing one is unsafe. This shape is
**platform-agnostic** — it applies the same way whether the
destination toolkit is Slack, Discord, Telegram, or any other
messaging app. Don't ask immediately — resolve it:
1. `search_tool_catalog { query, toolkit }` scoped to the TARGET
toolkit to find its user-lookup action — a "find user" / "lookup by
email" / "list users" style action, whatever that platform exposes
(never assume a slug across toolkits; always search for it).
2. Wire that lookup as a **`tool_call` node upstream of the send**.
3. Prefer an **email / exact lookup** when the platform offers one —
that's unambiguous, so bind its result directly with no question.
A **name search** can return multiple people: only bind it straight
through when it resolves to exactly one match; otherwise this is
bucket 3 — **ask the user to confirm which person / their email**
rather than messaging an unverified same-name match. If the
toolkit's lookup action can't resolve the person by name or email at
all, fall back to its "list users" style action plus a downstream
`transform`/`code` filter on an identifying field (email/display
name/etc).
4. Bind the resolved id into the send node's recipient arg with an `=`
expression off the lookup node — use `get_tool_contract` to find the
exact output field and confirm with `dry_run_workflow` rather than
guessing — same as the owner path above.
5. **Check the send action's own `get_tool_contract` for a required
"open conversation" step first.** Some messaging toolkits require
opening/creating a DM conversation for a user id before you can send
to it; others accept a user id as the recipient directly and
open/reuse the DM automatically. Never assume either way — if the
contract names a separate open/create-conversation action as a
prerequisite, wire that `tool_call` too, between the lookup and the
send.
Worked example (illustrative, not tied to one platform) — "every
Monday at 9am, message alan@acme.com his open tickets": `trigger`
(schedule, Mon 09:00) → `tool_call` `find_alan` (the target toolkit's
user-lookup action, args grounded via `get_tool_contract`, e.g. an
`email` arg) → `tool_call` fetching the tickets → (an
open-conversation `tool_call` first, only if that toolkit's contract
requires one) → `tool_call` `dm_alan` (the toolkit's send action,
recipient arg bound to `=nodes.find_alan.item.json.data.<id_field>`).
- Exactly one connected account for the toolkit the step needs → that
account (`list_flow_connections` / `composio_list_connections` tell
you this; don't ask "which Gmail?" when there's only one).