slackcraft
v0.0.4
Published
AI-to-Slack Block Kit tool library. Give any AI model native understanding of Slack's rich messaging capabilities.
Downloads
443
Maintainers
Readme
Slackcraft
A tools-first library that gives any AI model native understanding of Slack Block Kit. Developers plug in tool definitions, register app-specific actions, and the AI automatically decides when to use tables, buttons, rich cards, etc.
Table of Contents
- Why Slackcraft?
- Features
- Installation
- Quick Start
- Core API
- Built-in Tools
- Provider Adapters
- Auto-Chunking
- Plain Text Fallback
- Mrkdwn Cleanup
- Configuration Reference
- TypeScript Support
- FAQ
- Contributing
- License
Why Slackcraft?
Every company building AI assistants in Slack faces the same issue: AI models output plain text, but Slack supports rich, interactive layouts. Today, developers either:
- Post flat text that misses Slack's capabilities
- Hard-code Block Kit JSON for specific responses (doesn't scale)
- Use markdown-to-blocks converters that produce basic blocks without interactivity
None of these let the AI decide the right format. Slackcraft changes that — the AI natively decides the right layout for each response via function calling / tool use.
| Library | What it does | Slackcraft difference |
| ------------------- | ------------------------------- | ------------------------------------------------------ |
| @tryfabric/mack | Markdown → Block Kit | No AI awareness, no interactivity |
| slackify-markdown | Markdown → Slack mrkdwn | Doesn't produce Block Kit |
| jsx-slack | JSX → Block Kit | Developer-authored, not AI-driven |
| slackcraft | AI tool definitions → Block Kit | AI natively decides format; developer injects app data |
Features
- AI-Native — Tool definitions that any AI model can call via function calling / tool use. The AI decides when to use headers, lists, tables, buttons, etc.
- Provider Adapters — First-class support for OpenAI, Anthropic, and generic/LangChain setups. Write config once, get formatted tools for any provider.
- App-Specific Actions — Inject your app's buttons, links, and workflows into tool definitions. The AI knows what your app can do and surfaces relevant actions.
- Dynamic Values — AI can attach contextual data to buttons (user IDs, item IDs) that your action handlers receive.
- Mrkdwn Cleanup — AI is guided to use Slack mrkdwn, with automatic post-processing that catches common markdown mistakes (
**bold**→*bold*,[text](url)→<url|text>). - Auto-Chunking — Automatically splits responses that exceed Slack's 50-block / 12KB limit at natural boundaries.
- Plain Text Fallback — Auto-generates plain text from blocks for notifications and screen readers.
- Configurable Strictness — Lenient mode returns fallback blocks with warnings. Strict mode throws on invalid input. Your choice.
- Character Limit Enforcement — Respects Slack's per-field limits (150 for headers, 3000 for section text, 75 for button labels, etc.).
- Zero Dependencies — No runtime dependencies. Just TypeScript.
- TypeScript First — Full type coverage for config, tool parameters, Block Kit output, and results.
Installation
# npm
npm install slackcraft
# yarn
yarn add slackcraft
# pnpm
pnpm add slackcraftQuick Start
1. Create tools from your app config
import { createSlackTools } from "slackcraft"
const tools = createSlackTools({
actions: [
{
id: "recognize",
label: "🎉 Recognize a Peer",
description: "When the user wants to give kudos to a colleague",
action_id: "recognize_peer",
style: "primary",
},
{
id: "suggest",
label: "💡 Submit a Suggestion",
description: "When the user wants to submit anonymous feedback",
action_id: "open_suggestion_box",
},
{
id: "analytics",
label: "📊 View Analytics",
description: "When the user asks about team engagement or analytics",
url: "https://app.example.com/analytics",
},
],
globalContext:
"You are an employee engagement assistant. Be encouraging and supportive.",
})2. Pass tool definitions to your AI provider
import OpenAI from "openai"
const openai = new OpenAI()
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: "You are a helpful Slack assistant." },
{ role: "user", content: userMessage },
],
tools: tools.openai(),
})3. Resolve AI response to Slack blocks
import { resolveToolCalls } from "slackcraft"
const result = resolveToolCalls.fromOpenAI(response, tools)
await slack.chat.postMessage({
channel: channelId,
blocks: result.blocks,
text: result.text, // plain text fallback
})
// Handle overflow if the response was very long
if (result.overflow) {
for (const batch of result.overflow) {
await slack.chat.postMessage({
channel: channelId,
blocks: batch,
text: "(continued)",
})
}
}Core API
createSlackTools(config)
The main entry point. Call it once at app init with your configuration. Returns an object with methods to get tool definitions for any AI provider.
const tools = createSlackTools({
actions: [...], // App-specific action buttons
links: [...], // Static links the AI can reference
enabledTools: [...], // Which built-in tools to include (default: all)
globalContext: "...", // Extra instructions for AI behavior
strict: false, // Validation strictness (default: false)
});Returns:
| Method | Returns | Description |
| --------------------- | ------------------ | --------------------------------------------------- |
| tools.openai() | OpenAITool[] | Tool definitions in OpenAI function calling format |
| tools.anthropic() | AnthropicTool[] | Tool definitions in Anthropic tool use format |
| tools.generic() | GenericTool[] | Plain JSON Schema format (LangChain, custom setups) |
| tools.definitions() | ToolDefinition[] | Raw internal tool definitions |
| tools.config | SlackCraftConfig | The config object used to create these tools |
Tool definitions are cached after first call — no performance penalty for calling .openai() multiple times.
resolveToolCalls
Converts AI tool call responses into Slack Block Kit JSON. Provides provider-specific parsers.
// OpenAI
const result = resolveToolCalls.fromOpenAI(response, tools)
// Anthropic
const result = resolveToolCalls.fromAnthropic(response, tools)
// Generic (LangChain, custom)
const result = resolveToolCalls.fromGeneric(toolCalls, tools)Result shape:
interface SlackCraftResult {
blocks: SlackBlock[] // Block Kit blocks, ready to send
text: string // Plain text fallback for notifications
overflow: SlackBlock[][] | null // Extra payloads if over 50-block limit
toolsUsed: ToolName[] // Which tools the AI called
assistantText: string | null // Any plain text the AI returned alongside tools
warnings: SlackCraftWarning[] // Validation warnings (lenient mode)
}Built-in Tools
Each tool has a carefully crafted description that guides the AI on when to use it.
| Tool | When AI Uses It | Output |
| ------------------------ | ------------------------------------------------------------------ | ----------------------------------------------------- |
| slack_rich_message | Formatted text with headers, bold, lists, code blocks, blockquotes | Header, section, rich_text, divider blocks |
| slack_actions | User wants to do something (not just get info) | Action buttons populated from your registered actions |
| slack_fields_card | Structured key-value data (profiles, summaries, status reports) | Section blocks with fields in two-column layout |
| slack_table | Comparisons, tabular data, data summaries | Native Slack table block with columns and rows |
| slack_image | Response should include an image | Image block (URL or Slack file) |
| slack_video | Response should include a video | Video block with thumbnail and metadata |
| slack_markdown | Long-form AI-generated content (reports, explanations) | Markdown block (Slack renders natively) |
| slack_confirm | Needs user confirmation before a destructive/important action | Question section + confirm/deny action buttons |
| slack_plan | Multi-step AI agent workflows with task tracking | Plan block with task cards, statuses, sources |
| slack_context_actions | Lightweight feedback or quick-action buttons below a message | Context actions with feedback buttons or icon buttons |
| slack_composed_message | Complex responses combining multiple element types | Multiple block types (text, fields, tables, images) |
slack_rich_message
The default tool for any response needing visual structure.
Section types: header, text, code, quote, list, divider
// What the AI calls:
{
sections: [
{ type: "header", content: "Weekly Summary" },
{ type: "text", content: "Here's what happened this week:" },
{
type: "list",
items: ["Shipped v2.0", "Fixed 12 bugs", "3 new hires"],
ordered: false,
},
{ type: "divider" },
{ type: "quote", content: "Great work team! — CEO" },
{ type: "code", content: "const success = true;" },
]
}slack_actions
Presents action buttons based on the user's intent. The tool schema is dynamically generated from your registered actions — the AI sees what buttons your app has and picks the relevant ones.
// What the AI calls:
{
context_text: "Here are some things you can do:",
actions: [
{ id: "recognize", value: "user_U12345" },
{ id: "suggest" },
]
}The value field lets the AI attach contextual data (like a user ID) that your Slack action handler receives.
slack_fields_card
Displays structured information as key-value pairs in a two-column layout.
// What the AI calls:
{
title: "Team Status",
fields: [
{ label: "Members", value: "12" },
{ label: "Active", value: "10" },
{ label: "Satisfaction", value: "92%" },
{ label: "Last Survey", value: "2 days ago" },
],
footer: "Data from Q1 2025 survey",
image_url: "https://example.com/team-avatar.png"
}slack_table
Displays structured data in a native Slack table.
// What the AI calls:
{
title: "Sprint Progress",
columns: ["Task", "Owner", "Status"],
rows: [
["Auth service", "Alice", "Done"],
["Dashboard", "Bob", "In Progress"],
["API docs", "Carol", "Pending"],
],
footer: "Last updated: Monday"
}slack_image
Shows an image from a URL or Slack file.
// What the AI calls:
{
source: { type: "url", url: "https://example.com/chart.png" },
alt_text: "Q1 revenue chart",
title: "Revenue by Quarter"
}slack_video
Embeds a video with thumbnail and metadata.
// What the AI calls:
{
video_url: "https://example.com/demo.mp4",
thumbnail_url: "https://example.com/thumb.jpg",
title: "Product Demo",
alt_text: "Demo of the new feature",
author_name: "Product Team"
}slack_markdown
For long-form AI-generated content. Passes standard markdown through directly — Slack renders it natively. Ideal for reports, explanations, and documentation.
// What the AI calls:
{
text: "# Analysis Report\n\nThe data shows a **25% increase** in engagement...\n\n## Key Findings\n\n1. User retention improved\n2. Response times decreased\n3. Satisfaction scores at all-time high"
}slack_confirm
Asks the user to confirm or deny an action. Creates a question section with confirm/deny buttons.
// What the AI calls:
{
question: "Are you sure you want to delete this channel?",
confirm_action_id: "delete_channel",
confirm_label: "Yes, delete it",
deny_label: "No, keep it",
style: "danger"
}slack_plan
Displays a multi-step plan with task tracking. Perfect for AI agents that break work into steps.
// What the AI calls:
{
title: "Research Plan",
tasks: [
{ task_id: "1", title: "Search documentation", status: "complete" },
{
task_id: "2",
title: "Analyze results",
status: "in_progress",
details: "Reviewing top 10 matches...",
},
{ task_id: "3", title: "Write summary", status: "pending" },
]
}slack_context_actions
Adds lightweight buttons below a message — feedback thumbs, bookmark icons, etc.
// What the AI calls:
{
elements: [
{ type: "feedback_buttons", action_id: "msg_feedback" },
{
type: "icon_button",
action_id: "bookmark",
icon: "bookmark",
label: "Save",
value: "msg_123",
},
]
}slack_composed_message
The most powerful tool — composes a message from multiple part types. Use when a response needs a mix of text, fields, tables, images, and actions.
// What the AI calls:
{
parts: [
{ type: "text", content: "Here's the team summary:" },
{
type: "fields",
fields: [
{ label: "Members", value: "12" },
{ label: "Active", value: "10" },
],
},
{ type: "divider" },
{
type: "table",
columns: ["Name", "Role"],
rows: [
["Alice", "Eng"],
["Bob", "Design"],
],
},
{ type: "actions", action_ids: ["recognize"] },
]
}Provider Adapters
OpenAI
const tools = createSlackTools({ actions: [...] });
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [...],
tools: tools.openai(),
});
const result = resolveToolCalls.fromOpenAI(response, tools);Anthropic
const tools = createSlackTools({ actions: [...] });
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
messages: [...],
tools: tools.anthropic(),
});
const result = resolveToolCalls.fromAnthropic(response, tools);Generic (LangChain, custom)
const tools = createSlackTools({ actions: [...] });
// Get JSON Schema definitions for any framework
const toolDefs = tools.generic();
// After your AI returns tool calls in any format, normalize to:
const result = resolveToolCalls.fromGeneric([
{ name: "slack_rich_message", arguments: { sections: [...] } }
], tools);Auto-Chunking
Slack enforces a 50-block limit per message and approximately 12KB of JSON per payload. Slackcraft handles this automatically:
- After resolving tool calls, the library counts total blocks
- If under limits, everything goes in
result.blocks - If over, the response is split at natural boundaries (preferring divider blocks)
- Extra batches are returned in
result.overflow
const result = resolveToolCalls.fromOpenAI(response, tools)
// Send primary message
await slack.chat.postMessage({
channel,
blocks: result.blocks,
text: result.text,
})
// Send overflow if present
if (result.overflow) {
for (const batch of result.overflow) {
await slack.chat.postMessage({
channel,
blocks: batch,
text: "(continued)",
})
}
}Plain Text Fallback
Every result includes a text field — a plain text version of the blocks for Slack notifications and screen readers. This is generated automatically by stripping mrkdwn formatting and extracting text content from all blocks.
If the AI returns no tool calls (just plain text), Slackcraft automatically converts the text into basic section blocks with mrkdwn formatting. This means you always get valid blocks, whether the AI used tools or not.
Mrkdwn Cleanup
AI models are instructed via tool descriptions to use Slack's mrkdwn format. As a safety net, the library post-processes all text content to catch common mistakes:
| AI Output | Cleaned Output | Rule |
| ------------- | -------------- | ------------------------------------- |
| **bold** | *bold* | Double asterisks → single |
| __bold__ | *bold* | Double underscores → single asterisks |
| ~~strike~~ | ~strike~ | Double tildes → single |
| [text](url) | <url\|text> | Markdown links → Slack links |
| ### Header | Header | Strip header markers |
You can also use these utilities directly:
import { cleanMrkdwn, truncateText } from "slackcraft"
cleanMrkdwn("**bold** and [link](https://example.com)")
// → "*bold* and <https://example.com|link>"
truncateText("Very long text...", 150)
// → "Very long text..." (with "…" if truncated)Configuration Reference
SlackCraftConfig
| Property | Type | Default | Description |
| --------------- | ---------------- | ----------- | ----------------------------------------------------------------- |
| actions | ActionConfig[] | undefined | App-specific action buttons the AI can offer |
| links | LinkConfig[] | undefined | Static links the AI can reference |
| enabledTools | ToolName[] | All tools | Which built-in tools to include |
| globalContext | string | undefined | Extra instructions injected into every tool description |
| strict | boolean | false | If true, throws on invalid input. If false, returns warnings. |
ActionConfig
| Property | Type | Required | Description |
| ------------- | ----------------------- | --------------------------- | ---------------------------------------------------------------------- |
| id | string | Yes | Unique identifier for this action |
| label | string | Yes | Button display text (e.g., "🎉 Recognize a Peer") |
| description | string | Yes | When the AI should offer this action (injected into tool descriptions) |
| action_id | string | One of action_id or url | Slack action_id for your app's handler |
| url | string | One of action_id or url | URL to open (makes this a link button) |
| style | "primary" \| "danger" | No | Button style |
LinkConfig
| Property | Type | Required | Description |
| ------------- | -------- | -------- | -------------------------------------- |
| id | string | Yes | Unique identifier |
| label | string | Yes | Display label |
| url | string | Yes | URL to open |
| description | string | Yes | When the AI should reference this link |
Error Handling
In lenient mode (default), invalid tool parameters produce fallback output with warnings:
const result = resolveToolCalls.fromOpenAI(response, tools)
if (result.warnings.length > 0) {
console.warn("Slackcraft warnings:", result.warnings)
// [{ tool: "slack_actions", message: "Unknown action ID \"invalid\"", field: "actions.id" }]
}In strict mode, invalid parameters throw errors:
const tools = createSlackTools({ strict: true, actions: [...] });
// Throws: Error: Unknown action ID "invalid". Valid IDs: recognize, suggestTypeScript Support
All public APIs are fully typed. Import any type you need:
import type {
// Config
SlackCraftConfig,
ActionConfig,
LinkConfig,
// Tool definitions
ToolName,
ToolDefinition,
SlackTools,
// Provider formats
OpenAITool,
AnthropicTool,
GenericTool,
GenericToolCall,
// Tool parameters (what the AI passes)
RichMessageParams,
RichMessageSection,
ActionsParams,
FieldsCardParams,
TableParams,
ImageParams,
VideoParams,
MarkdownParams,
ConfirmParams,
PlanParams,
TaskCardParams,
ContextActionsParams,
ComposedMessageParams,
ComposedPart,
// Result
SlackCraftResult,
SlackCraftWarning,
// Slack Block Kit output
SlackBlock,
SlackHeaderBlock,
SlackSectionBlock,
SlackDividerBlock,
SlackImageBlock,
SlackVideoBlock,
SlackMarkdownBlock,
SlackTableBlock,
SlackPlanBlock,
SlackTaskCardBlock,
SlackContextActionsBlock,
SlackContextBlock,
SlackActionsBlock,
SlackRichTextBlock,
SlackTextObject,
SlackButtonElement,
SlackConfirmDialog,
SlackElement,
} from "slackcraft"FAQ
What happens if the AI doesn't call any tools?
If the AI returns plain text without any tool calls, Slackcraft automatically converts it into section blocks with mrkdwn formatting. You always get valid blocks in the result.
Can I use this with LangChain?
Yes. Use tools.generic() to get JSON Schema tool definitions, and resolveToolCalls.fromGeneric() to resolve tool calls. Works with any framework that supports JSON Schema tools.
What if the AI calls a tool with invalid parameters?
In lenient mode (default), Slackcraft returns partial/fallback blocks and populates the warnings array. In strict mode, it throws an error. Configure via strict: true in your config.
Does this work with streaming?
Streaming support is planned for a future release. Currently, you need the complete AI response before calling resolveToolCalls.
Why not just convert markdown to Block Kit?
Markdown converters are reactive — they take whatever the AI outputs and try to make it look good. Slackcraft is proactive — the AI knows what Slack can do and chooses the right format. This means the AI can decide "this should be action buttons" or "this needs a fields card" rather than everything being plain text.
How do I add custom tools beyond the built-ins?
Custom tool registration is planned for a future release. For now, you can handle custom tools alongside Slackcraft by intercepting tool calls before passing them to resolveToolCalls.
Can I use only specific tools?
Yes. Use the enabledTools option to pick exactly which tools your AI gets:
const tools = createSlackTools({
actions: [...],
enabledTools: ["slack_rich_message", "slack_actions", "slack_fields_card"],
});Only the specified tools will be included in the definitions sent to the AI. The slack_actions tool is automatically excluded if you don't provide any actions in the config.
Does this handle Slack's rate limits?
No. Slackcraft handles Block Kit formatting and chunking. Rate limiting, retries, and queueing are the responsibility of your Slack client (e.g., @slack/web-api).
Can I use this without an AI model?
Technically yes — you can call resolveToolCalls.fromGeneric() with manually constructed tool calls. But the library is designed around AI tool calling as the primary use case.
Contributing
Contributions are welcome! Here's how you can help:
- Report bugs — Open an issue with reproduction steps
- Request features — Describe your use case and the tool/feature you'd like
- Submit PRs — Fork, branch, and submit a pull request
- Improve docs — Fix typos, add examples, clarify explanations
