Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Block Kit support

Slack Block Kit messages and views are JSON documents built from a fixed set of block and element types. This library models each of those types as a Rust struct or enum, and provides a small set of macros — slack_blocks!, md!, pt! — to build the Vec of blocks or elements each block-holding type expects, without writing .into() on every item by hand.

Everything below is real, compiling code. Run it yourself from examples/blocks_showcase.rs, or copy any snippet directly.

From JSON to Rust

Take a very simple block, as Slack’s Block Kit Builder shows it:

{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "A message *with some bold text* and _some italicized text_."
      }
    }
  ]
}

The same block in type-safe Rust is one struct per JSON object, with the type tag implied by the struct, and md! standing in for the mrkdwn text object:

use slack_morphism::prelude::*;

let blocks: Vec<SlackBlock> = slack_blocks![
    SlackSectionBlock::new()
        .with_text(md!("A message *with some bold text* and _some italicized text_.")),
];

Every block, element and composition object follows this pattern: a JSON object becomes SlackXxx::new(required fields), each optional key becomes a .with_key(..) call, and a JSON array of blocks or elements becomes a slack_blocks![..] list.

Quick start

The smallest possible message is a single section block with some markdown text, wrapped in a SlackMessageContent and posted with SlackApiChatPostMessageRequest:

use slack_morphism::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = SlackClient::new(SlackClientHyperConnector::new()?);
let token: SlackApiToken = SlackApiToken::new("xoxb-your-token".into());
let session = client.open_session(&token);

let blocks: Vec<SlackBlock> = slack_blocks![
    SlackSectionBlock::new().with_text(md!("A message *with some bold text*.")),
];

let content = SlackMessageContent::new().with_blocks(blocks);
let request = SlackApiChatPostMessageRequest::new("#general".into(), content);

session.chat_post_message(&request).await?;
Ok(())
}

The rest of this page builds up from here: how text objects work, how slack_blocks! builds lists, and how each block and element family is constructed.

Text objects

Block Kit has two flavors of text: mrkdwn (Slack’s own markdown) and plain_text. The library models them as SlackBlockMarkDownText and SlackBlockPlainText, and the md! and pt! macros build them:

use slack_morphism::prelude::*;

let markdown: SlackBlockMarkDownText = md!("A message *with some bold text*.");
let plain: SlackBlockPlainText = pt!("Plain text");

// Both macros format like `format!` when given more than one argument.
let user_id = SlackUserId("U1234".into());
let greeting: SlackBlockMarkDownText = md!("Hey {}, welcome!", user_id.to_slack_format());

md! and pt! convert into whatever type the call site expects via Into, so the same macro call can produce a SlackBlockText, a SlackContextBlockElement, or the raw SlackBlockMarkDownText/ SlackBlockPlainText, depending on where it is used.

Some fields require plain text specifically and reject markdown at the API level: block headers, button labels, and input labels are typed as SlackBlockPlainTextOnly, not SlackBlockText. Passing markdown there does not fail to compile — a plain string still converts — but Slack rejects the request at runtime with invalid_blocks if the string contains markdown syntax Slack cannot render as plain text. Use pt!, or a bare string literal, for these fields.

Building block lists

slack_blocks! builds a Vec<T> from a comma-separated list, converting each item with .into(). Any block or element type that has a From (or Into) conversion to the target item type can appear as a bare item:

use slack_morphism::prelude::*;

let show_divider = true;

let blocks: Vec<SlackBlock> = slack_blocks![
    SlackHeaderBlock::new(pt!("Weekly report")),
    optionally(show_divider => SlackDividerBlock::new()),
    ..(1..=3).map(|n| SlackSectionBlock::new().with_text(md!("Item {}", n))),
];

assert_eq!(blocks.len(), 5);
  • A bare expression is pushed via .into().
  • optionally(pred => item) converts and pushes item the same way a bare item does, only when pred is true; neither pred nor item is evaluated when it is false.
  • ..iter splices every element of an iterator or Vec in, each converted via .into() — useful for building a run of blocks from data instead of writing them out by hand, or for spreading in blocks assembled elsewhere, as in Templates below.

These forms mix freely in one list. An older some(item) / some_into(item) / optionally_into(pred => item) syntax still works and appears throughout older code and examples; prefer the bare form above in new code.

One limit worth knowing up front: a bare md!(..) or pt!(..) call cannot be a list item directly, because its expansion already ends in an untyped .into() and the compiler has nothing left to infer the item type from (E0283). This only matters for a list whose items are text objects themselves, such as a context block’s elements or a section’s fields — give the list an explicit element type instead, as in SlackContextBlock::new(vec![md!("hi")]) or .with_fields(vec![md!("hi")]), both used below.

Sections

A section block carries a text object, optional fields (short text/value pairs laid out in a grid), and an optional accessory element:

use slack_morphism::prelude::*;

let section = SlackSectionBlock::new()
    .with_text(md!("*Deploy status*: all systems green"))
    .with_fields(vec![md!("*Region:*\nus-east-1"), md!("*Duration:*\n42s")])
    .with_accessory(
        SlackBlockButtonElement::new("view-details".into(), pt!("Details")).into(),
    );

let block: SlackBlock = section.into();

Actions and inputs

Action blocks hold interactive elements — buttons, selects, pickers. Input blocks pair one element with a required label, for use in modals.

use slack_morphism::prelude::*;

let approve = SlackBlockButtonElement::new("approve".into(), pt!("Approve"))
    .with_style(SlackBlockButtonStyle::Primary);
let deny = SlackBlockButtonElement::new("deny".into(), pt!("Deny"))
    .with_style(SlackBlockButtonStyle::Danger);

let actions: SlackBlock = SlackActionsBlock::new(slack_blocks![approve, deny]).into();

let region_select = SlackBlockStaticSelectElement::new("region".into())
    .with_placeholder(pt!("Choose a region"))
    .with_options(vec![
        SlackBlockChoiceItem::new("US East".into(), "us-east-1".into()),
        SlackBlockChoiceItem::new("EU West".into(), "eu-west-1".into()),
    ]);

let date_picker = SlackBlockDatePickerElement::new("deploy-date".into())
    .with_placeholder(pt!("Pick a date"));

let date_input: SlackBlock =
    SlackInputBlock::new("Deploy date".into(), date_picker.into()).into();

let _ = (actions, region_select, date_input);

Context

Context blocks hold a small run of text and image elements, usually shown in a muted style below other content. As noted above, its element list needs an explicit element type, so bare md!/pt! calls work directly inside vec![...] without going through slack_blocks!:

use slack_morphism::prelude::*;

let context: SlackBlock =
    SlackContextBlock::new(vec![md!("Posted by "), pt!("the release bot")]).into();

Rich text

Rich text blocks are Slack’s structured formatting model: sections made of inline runs (text, links, users, emoji, …), lists, quotes, and preformatted code. A bare string converts into an unstyled text run wherever an inline element is expected, and SlackRichTextText has .bold(), .italic(), .strike(), and .code() helpers for styled runs:

use slack_morphism::prelude::*;

let section: SlackRichTextElement = SlackRichTextSection::new(vec![
    "Build ".into(),
    SlackRichTextText::new("passed".to_string()).bold().into(),
    " ".into(),
    SlackRichTextEmoji::new(SlackEmojiName("white_check_mark".into())).into(),
])
.into();

let list: SlackRichTextElement = SlackRichTextList::new(
    SlackRichTextListStyle::Bullet,
    vec!["Unit tests".into(), "Integration tests".into()],
)
.into();

let quote: SlackRichTextElement =
    SlackRichTextQuote::new(vec!["Ship it.".into()]).into();

let preformatted: SlackRichTextElement =
    SlackRichTextPreformatted::new(vec!["cargo test --doc".into()]).into();

let rich_text: SlackBlock =
    SlackRichTextBlock::new(vec![section, list, quote, preformatted]).into();

The same bare-string conversion applies inside a SlackRichTextList’s items and a rich text table cell, below.

Tables

Table blocks are a Vec<Vec<SlackTableCell>>. A bare string becomes a raw-text cell; a SlackTableRichTextCell holds rich text elements the same way a rich text block does:

use slack_morphism::prelude::*;

let table: SlackBlock = SlackTableBlock::new(vec![
    vec!["Service".into(), "Status".into()],
    vec![
        "api".into(),
        SlackTableRichTextCell::new(vec![SlackRichTextSection::new(vec![
            SlackRichTextText::new("healthy".to_string()).bold().into(),
        ])
        .into()])
        .into(),
    ],
])
.into();

Templates

SlackMessageTemplate renders a full SlackMessageContent; SlackBlocksTemplate renders just a Vec<SlackBlock> meant to be spread into a larger message with ... A typical template takes its parameters as a struct built with rsb_derive::Builder:

use slack_morphism::prelude::*;
use rsb_derive::Builder;

#[derive(Debug, Clone, Builder)]
struct DeploySummaryParams {
    service: String,
    ok: bool,
}

impl SlackBlocksTemplate for DeploySummaryParams {
    fn render_template(&self) -> Vec<SlackBlock> {
        slack_blocks![
            SlackDividerBlock::new(),
            SlackContextBlock::new(vec![md!("Deployed by the release bot")]),
        ]
    }
}

#[derive(Debug, Clone, Builder)]
struct WelcomeMessageParams {
    user_id: SlackUserId,
}

impl SlackMessageTemplate for WelcomeMessageParams {
    fn render_template(&self) -> SlackMessageContent {
        let footer = DeploySummaryParams::new("api".into(), true);

        SlackMessageContent::new()
            .with_text(format!("Hey {}", self.user_id.to_slack_format()))
            .with_blocks(slack_blocks![
                SlackSectionBlock::new()
                    .with_text(md!("Hey {}, welcome!", self.user_id.to_slack_format())),
                ..footer.render_template(),
            ])
    }
}

let message = WelcomeMessageParams::new("U1234".into());
let content = message.render_template();
assert_eq!(content.blocks.map(|b| b.len()), Some(3));

Views and modals

A modal is a SlackModalView: a title, a list of blocks (typically inputs), and optional submit/close button text. Opening one requires a trigger_id from the interaction that triggered it — a button click or a shortcut invocation, not something you can obtain outside of a live request. Publishing a home tab view uses views.publish and a user id instead:

use slack_morphism::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = SlackClient::new(SlackClientHyperConnector::new()?);
let token: SlackApiToken = SlackApiToken::new("xoxb-your-token".into());
let session = client.open_session(&token);

let name_input = SlackBlockPlainTextInputElement::new("name".into());

let modal = SlackModalView::new(
    "Deploy".into(),
    vec![SlackInputBlock::new("Service name".into(), name_input.into()).into()],
)
.with_submit(pt!("Deploy"));

let open_request = SlackApiViewsOpenRequest::new(
    SlackTriggerId("123.456.abcdef".into()),
    SlackView::Modal(modal),
);
session.views_open(&open_request).await?;

let home = SlackHomeView::new(vec![SlackSectionBlock::new()
    .with_text(md!("Welcome to your home tab."))
    .into()]);

let publish_request =
    SlackApiViewsPublishRequest::new(SlackUserId("U1234".into()), SlackView::Home(home));
session.views_publish(&publish_request).await?;
Ok(())
}

Troubleshooting invalid_blocks

Slack validates blocks server-side and returns invalid_blocks with little detail beyond that. The usual causes:

  • Markdown where plain text is required. Header text, button labels, input labels, and similar SlackBlockPlainTextOnly fields do not render mrkdwn syntax; passing text with *bold* or a link in one of them either shows the literal asterisks or is rejected outright, depending on the field.
  • Text over 3000 characters. Section and context text objects have a 3000-character limit; button and option text has a much smaller one (75 characters for a button label).
  • More than 50 blocks in one message, or more than 100 in a modal or home tab.
  • A missing or duplicate action_id. Every interactive element needs an action_id unique within its view or message; the button, select, and input constructors above all take it as their first argument for this reason.

When none of the above explains it, the fastest way to isolate a bad block is to paste the JSON from Block Kit Builder straight into serde_json::from_str::<Vec<SlackBlock>>, confirm it deserializes and matches what you expected, then compare it field by field against the blocks you built:

use slack_morphism::prelude::*;

let payload = r#"[
    {
        "type": "section",
        "text": { "type": "mrkdwn", "text": "A message *with some bold text*." }
    }
]"#;

let blocks: Vec<SlackBlock> = serde_json::from_str(payload)?;
assert_eq!(blocks.len(), 1);
Ok::<(), serde_json::Error>(())

Convert Block Kit Builder JSON

Paste the JSON from Block Kit Builder below and press Convert (or Ctrl+Enter) to get the equivalent builder code. The converter runs entirely in your browser: nothing is uploaded anywhere. It accepts a Builder message ({"blocks": [...]}), a bare block array, a single block, or a modal or home view; anything the crate cannot model is reported as an error rather than guessed at.

The converter runs on the documentation site; open this page there to use it.

The same conversion is available as a command-line tool:

cargo run --manifest-path tools/blockkit-to-rust/Cargo.toml --bin bk2rs < message.json

More examples

examples/blocks_showcase.rs posts one message per block family covered on this page and prints a modal view. examples/client.rs shows a complete, runnable client setup including authentication. examples/socket_mode.rs shows handling interactions (button clicks, view submissions) that come back from the blocks you post.