Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci-scripts-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- ".github/workflows/ci-scripts-test.yml"
- "pyproject.toml"
- "src/bundles/*/pyproject.toml"
- "src/bundles/*/src/*/**/capabilities.v1.json"
- "src/bundles/lfx-bundles/src/lfx_bundles/*/__init__.py"
- "src/backend/base/langflow/api/build.py"
- "src/backend/base/langflow/api/v1/a2a.py"
Expand Down
14 changes: 14 additions & 0 deletions BUNDLE_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,17 @@ the deserialize half is covered by
messages and winner selection are unchanged, and two physically distinct
manifests for one canonical name still error. No public symbol's name or
signature changed.
- **`ResolvedCredential.identity` (additive, optional).**
`lfx.integrations.models.ResolvedCredential` gained
`identity: Literal["user_delegated", "bot", "service"] | None = None`,
mirroring `lfx.integrations.capabilities.IntegrationIdentity`. The
database-backed resolver populates it from the connection row's
`executing_identity`; the headless environment resolver leaves it `None`
because the `LF_CONNECTION__*` wire format has no place to declare one.
Providers whose user and bot tokens share scope names — Slack's `chat:write`
is both a User Token Scope and a Bot Token Scope — cannot distinguish the two
identities from `granted_scopes`, so a bundle capability that must run as a
bot compares this field and fails closed with `connection-not-authorized`
before its first request. The field defaults to `None`, no existing field
changed name, type, or meaning, and every existing construction site keeps
working, so `BUNDLE_API_VERSION` remains `1`.
115 changes: 115 additions & 0 deletions docs/docs/Components/bundles-slack.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: Slack
slug: /bundles-slack
---

import Icon from "@site/src/components/icon";
import { GraduatedBundleInstall } from '@site/docs/_partial-bundle-graduated-install.mdx';

<Icon name="Blocks" aria-hidden="true" /> [**Bundles**](/components-bundle-components) contain custom components that support specific third-party integrations with Langflow.

This page describes the components that are available in the **Slack** bundle.

Every Slack component runs on the [Slack Web API](https://docs.slack.dev/reference/methods/) and takes its credential from a Langflow [connection](../Develop/connection-oauth.mdx) rather than from a token pasted into the flow.

<GraduatedBundleInstall packageName="slack" />

## Two executing identities

Slack has two kinds of token, and a component is fixed to one of them:

| Identity | Authorization profile | Posts as | Available on Desktop |
| --- | --- | --- | --- |
| Connected Slack user | `slack-user-oauth` | the person who authorized the connection | Yes |
| App bot user | `slack-bot-install` | the app's bot user | No |

Slack user scopes and bot scopes share names — `chat:write` is both a User Token Scope and a Bot Token Scope — so the scopes a connection was granted cannot tell the two identities apart. Each component instead checks the identity recorded on the connection and fails with a connection authorization error, before any request reaches Slack, when it is handed the wrong kind.

Bot components are unavailable on Langflow Desktop because Slack desktop redirects use PKCE and [may not request bot scopes](https://docs.slack.dev/authentication/using-pkce/). Use a hosted or self-managed workspace installation for those actions.

## Prerequisites

* A Slack app installed in the target workspace, and a Langflow connection for it. See [Configure connection OAuth](../Develop/connection-oauth.mdx) for the registration steps, the redirect URLs, and the per-action scope tables.
* For bot components, the app's bot user must be a member of the channel it acts on.

## Slack: Search (as user)

Calls [`search.messages`](https://docs.slack.dev/reference/methods/search.messages) with the connected user's visibility. Accepts the same query modifiers as the Slack search bar, such as `in:#general from:@avery`.

Required scope: `search:read`.

Outputs a list of [`Data`](/data-types#json) objects, one per match, and a `Pagination` `Data` carrying Slack's result counts and the `next_cursor` for the following page.

## Slack: Read Thread (as user)

Calls [`conversations.replies`](https://docs.slack.dev/reference/methods/conversations.replies) for one thread, identified by its channel and the parent message's `ts`.

Required scopes: `channels:history`, `groups:history`, `im:history`, `mpim:history`. All four are requested so one connection can read threads in public channels, private channels, DMs, and group DMs.

Outputs the thread's messages as `Data` objects and a `Pagination` `Data` with `has_more` and `next_cursor`.

:::important
For apps that are distributed but **not** listed in the Slack Marketplace, Slack rate-limits `conversations.replies` to one request per minute and caps each page at 15 messages. Marketplace-listed apps get Tier 3 (50+ per minute). A customer-owned app used only inside its own workspace is not affected.
:::

## Slack: Send Message (as user)

Calls [`chat.postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage) with the user token, so the message is attributed to the connected person.

Required scope: `chat:write`.

Set **Thread timestamp** to reply inside a thread. Optional **Blocks** accepts [Block Kit](https://docs.slack.dev/block-kit/) blocks as `Data` objects. Slack truncates message text above 40,000 characters, so the component rejects longer text before sending.

Outputs a `Data` with the `channel`, `ts`, and the stored `message`.

## Slack: Create Canvas (as user)

Calls [`canvases.create`](https://docs.slack.dev/reference/methods/canvases.create) with markdown content, up to 1 MiB. The canvas is owned by the connected user.

Required scope: `canvases:write`.

Set **Channel ID** to create a channel canvas instead of a standalone one; free Slack plans cannot create standalone canvases.

Outputs a `Data` with the new `canvas_id`.

## Slack: Post Message (as app)

Calls `chat.postMessage` with the bot token, so the message is attributed to the app's bot user rather than to a person.

Required scope: `chat:write` (bot).

Adds **Also send to channel** (`reply_broadcast`) and **Attachments** on top of the user variant's inputs.

## Slack: Add Reaction (as app)

Calls [`reactions.add`](https://docs.slack.dev/reference/methods/reactions.add) as the bot user. Give the emoji name without colons, such as `thumbsup`.

Required scope: `reactions:write`.

## Slack: List Channel Members (as app)

Calls [`conversations.members`](https://docs.slack.dev/reference/methods/conversations.members) as the bot user.

Required scope: `channels:read`. Two scopes are requested only when you enable the matching option:

| Option | Additional scope |
| --- | --- |
| **Private channel** | `groups:read` |
| **Resolve display names** | `users:read` |

With **Resolve display names** off, the component returns member IDs. With it on, it calls [`users.info`](https://docs.slack.dev/reference/methods/users.info) once per member and returns the name, real name, display name, and bot flag as well — which costs one extra request per member, so leave it off for large channels.

Outputs the members as `Data` objects and a `Pagination` `Data` with `next_cursor`.

## Errors

Slack reports application-level failures as `HTTP 200` with `{"ok": false, ...}`. The bundle translates those into Langflow's integration error codes, so the canvas shows an actionable message instead of a generic provider outage:

| Slack error | Langflow error | What to do |
| --- | --- | --- |
| `invalid_auth`, `token_expired`, `token_revoked`, `account_inactive`, `not_authed` | `auth-expired` | Reconnect the connection. |
| `missing_scope` | `scope-missing` | Grant the listed scopes and reconnect. |
| `ratelimited` | `rate-limited` | Retry after the reported interval. |
| `not_allowed_token_type`, `channel_not_found`, `not_in_channel` | `action-unsupported` | Check the identity, the channel, and the bot's membership. |

If Slack rejects a token, the component re-resolves the connection once and retries. That is how a rotated Slack token is picked up: Slack tokens do not expire unless the app opted into token rotation, so a rejection is the only signal.
81 changes: 81 additions & 0 deletions docs/docs/Develop/connection-oauth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,84 @@ action would fail every Microsoft resolution.
Only customer-owned single-tenant registrations are configurable today, since
this broker uses a fixed-tenant authority. A Langflow-owned multitenant
application, which also needs Entra publisher verification, is a follow-up.

{/* INT-12 (LE-2470): appended section. Keep additions to this page as new
trailing sections so stacked integration branches do not collide. */}

## Slack redirect URLs and scopes per action

### Redirect URLs

Register both forms the deployment needs. A server redirect is used for hosted and
self-managed instances; the loopback form is used by the Desktop PKCE app.

```text
https://YOUR_INSTANCE/api/v1/connections/oauth/slack/callback
http://localhost:7860/api/v1/connections/oauth/slack/callback
```

### Scopes per action

The `lfx-slack` bundle declares these scopes per action. A registration's `scopes`
array is the operator ceiling, so it must be a superset of the actions you intend to
enable; the connection then requests the subset an action needs.

| Action | Identity | Registration | Scopes |
| --- | --- | --- | --- |
| Search | user | `profile: "user"` | `search:read` |
| Read Thread | user | `profile: "user"` | `channels:history`, `groups:history`, `im:history`, `mpim:history` |
| Send Message | user | `profile: "user"` | `chat:write` |
| Create Canvas | user | `profile: "user"` | `canvases:write` |
| Post Message | bot | `profile: "bot"` | `chat:write` |
| Add Reaction | bot | `profile: "bot"` | `reactions:write` |
| List Channel Members | bot | `profile: "bot"` | `channels:read`, plus `groups:read` for private channels and `users:read` to resolve display names |

User Token Scopes and Bot Token Scopes are configured separately in the Slack app,
and `chat:write` appears in both lists. Because the two token types cannot be told
apart from their granted scopes, keep one registration and one connection per
identity, and name them accordingly.

A worked two-registration example, covering every action above:

```json
{
"slack-user": {
"provider": "slack",
"profile": "user",
"owner": "customer",
"context": "self_managed",
"client_type": "confidential",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"redirect_uri": "https://langflow.example/api/v1/connections/oauth/slack/callback",
"allowed_tenants": ["T0SLACKTEAM"],
"scopes": [
"search:read",
"channels:history",
"groups:history",
"im:history",
"mpim:history",
"chat:write",
"canvases:write"
]
},
"slack-bot": {
"provider": "slack",
"profile": "bot",
"owner": "customer",
"context": "self_managed",
"client_type": "confidential",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"redirect_uri": "https://langflow.example/api/v1/connections/oauth/slack/callback",
"allowed_tenants": ["T0SLACKTEAM"],
"scopes": ["chat:write", "reactions:write", "channels:read", "groups:read", "users:read"]
}
}
```

Slack desktop redirects may not request bot scopes, so the Desktop PKCE app carries
only the user scopes; bot actions are unavailable there. For headless runtimes,
provide the token directly as `LF_CONNECTION__SLACK__<NAME>` instead of registering
an OAuth app. That path records no executing identity, so the bundle trusts the
operator to supply the right token type.
1 change: 1 addition & 0 deletions docs/docs/Lfx/extensions-bundle-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The default `uv pip install langflow` includes the following curated standalone
| [`openai`](/bundles-openai) (OpenAI) | `uv pip install lfx-openai` |
| [`openai-compatible`](/bundles-openai-compatible) (OpenAI Compatible) | `uv pip install lfx-openai-compatible` |
| [`oracle`](/bundles-oracle) (Oracle) | `uv pip install lfx-oracle` |
| [`slack`](../Components/bundles-slack.mdx) (Slack) | `uv pip install lfx-slack` |
| [`vllm`](/bundles-vllm) (vLLM) | `uv pip install lfx-vllm` |

## Opt-in standalone packages
Expand Down
1 change: 1 addition & 0 deletions docs/docs/_partial-bundle-graduated-install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const GraduatedBundleInstall = ({ packageName }) => {
'openai-compatible': { name: 'OpenAI Compatible', coreSupport: false, inDefault: true },
'oracle': { name: 'Oracle', coreSupport: false, inDefault: true },
'paddle': { name: 'PaddleOCR', coreSupport: false, inDefault: false },
'slack': { name: 'Slack', coreSupport: false, inDefault: true },
'valkey': { name: 'Valkey', coreSupport: false, inDefault: false },
'vllm': { name: 'vLLM', coreSupport: false, inDefault: true },
};
Expand Down
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ module.exports = {
"Components/bundles-sambanova",
"Components/bundles-searchapi",
"Components/bundles-serper",
"Components/bundles-slack",
"Components/bundles-supabase",
"Components/bundles-upstash",
"Components/bundles-valkey",
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies = [
"lfx-openai-compatible>=0.1.5,<1.0.0",
"lfx-google>=0.1.1,<1.0.0",
"lfx-microsoft>=0.1.0,<1.0.0",
"lfx-slack>=0.1.0,<1.0.0",
# langflow-extensions:bundle-deps-end
]

Expand Down Expand Up @@ -125,6 +126,7 @@ lfx-valkey = { workspace = true }
lfx-google = { workspace = true }
lfx-confluent = { workspace = true }
lfx-microsoft = { workspace = true }
lfx-slack = { workspace = true }
# langflow-extensions:bundle-sources-end
torch = { index = "pytorch-cpu" }
torchvision = { index = "pytorch-cpu" }
Expand Down Expand Up @@ -162,6 +164,7 @@ members = [
"src/bundles/google",
"src/bundles/confluent",
"src/bundles/microsoft",
"src/bundles/slack",
# langflow-extensions:bundle-members-end
]

Expand Down
13 changes: 12 additions & 1 deletion scripts/ci/bundle_profile_locks/enterprise-hardened.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@
"requirement": "lfx-oracle==0.1.3",
"resolved_version": "0.1.3"
},
{
"distribution": "lfx-slack",
"extras": [],
"providers": [
"lfx-slack"
],
"requested": ">=0.1.0,<1.0.0",
"requirement": "lfx-slack==0.1.0",
"resolved_version": "0.1.0"
},
{
"distribution": "lfx-toolguard",
"extras": [],
Expand Down Expand Up @@ -170,7 +180,7 @@
"https://pypi.org/simple"
],
"profile": "enterprise-hardened",
"profile_digest": "sha256:b8d049b906f47a32",
"profile_digest": "sha256:ad31caea4d8e310d",
"providers": [
"lfx-amazon",
"lfx-anthropic",
Expand All @@ -185,6 +195,7 @@
"lfx-openai",
"lfx-openai-compatible",
"lfx-oracle",
"lfx-slack",
"lfx-toolguard",
"lfx-vllm"
],
Expand Down
8 changes: 8 additions & 0 deletions scripts/ci/bundle_profiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@
"providers": [
"lfx-microsoft"
]
},
{
"distribution": "lfx-slack",
"extras": [],
"version": ">=0.1.0,<1.0.0",
"providers": [
"lfx-slack"
]
}
]
}
Expand Down
Loading
Loading