Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Slack By James Joyner IV · · 9 min read

Slack Modals and Interactive Components for Ops Tooling

Slash commands are fine for simple actions, but real ops workflows need input. Here's how to use modals, select menus, and multi-step views to build serious tooling.

  • #slack
  • #modals
  • #block-kit
  • #interactivity
  • #devops
  • #chatops

A slash command is great for “do one thing”: /deploy status, /oncall now. But the moment your ops action needs input — which service, which environment, a reason for the change, a confirmation — cramming it into command text gets ugly fast. /deploy checkout-api production --reason="hotfix for tax bug" --skip-canary is not a UX; it’s a syntax error waiting to happen. This is exactly what modals are for. They turn a chat command into a real form, with validation, dropdowns, and structured submission.

Here’s how I build interactive ops tooling with modals and Block Kit input components.

When to reach for a modal

Use a modal when the action has more than one parameter, when any parameter is risky enough to want confirmation, or when you want validation before anything executes. A modal gives you a clean form, server-side validation, and a single structured payload on submit — far better than parsing free text.

The lifecycle is three steps: a trigger opens the modal (views.open), the user fills it in, and Slack sends you a view_submission event with all the values. You validate, run the action, and either close the modal or show inline errors.

Opening a modal

A modal must be opened with a trigger_id, which Slack gives you on any interaction — a slash command, a button click — and which expires in about 3 seconds. So open the modal first, before any slow work.

app.command('/deploy', async ({ ack, body, client }) => {
  await ack();
  await client.views.open({
    trigger_id: body.trigger_id,        // use it immediately
    view: deployModal(),
  });
});

A real deploy modal

Here’s a modal that collects a service, an environment, and a required reason — with the reason field enforced server-side:

{
  "type": "modal",
  "callback_id": "deploy_submit",
  "title": { "type": "plain_text", "text": "Deploy a service" },
  "submit": { "type": "plain_text", "text": "Deploy" },
  "blocks": [
    {
      "type": "input",
      "block_id": "service",
      "label": { "type": "plain_text", "text": "Service" },
      "element": {
        "type": "static_select",
        "action_id": "value",
        "options": [
          { "text": { "type": "plain_text", "text": "checkout-api" }, "value": "checkout-api" },
          { "text": { "type": "plain_text", "text": "payments-api" }, "value": "payments-api" }
        ]
      }
    },
    {
      "type": "input",
      "block_id": "env",
      "label": { "type": "plain_text", "text": "Environment" },
      "element": {
        "type": "static_select",
        "action_id": "value",
        "options": [
          { "text": { "type": "plain_text", "text": "staging" }, "value": "staging" },
          { "text": { "type": "plain_text", "text": "production" }, "value": "production" }
        ]
      }
    },
    {
      "type": "input",
      "block_id": "reason",
      "label": { "type": "plain_text", "text": "Reason" },
      "element": { "type": "plain_text_input", "action_id": "value", "multiline": true }
    }
  ]
}

Handling submission and validating server-side

On submit, Slack sends view_submission with state.values. Pull the fields, validate, and — critically — return inline errors instead of executing if something’s wrong:

app.view('deploy_submit', async ({ ack, body, view, client }) => {
  const vals = view.state.values;
  const service = vals.service.value.selected_option.value;
  const env = vals.env.value.selected_option.value;
  const reason = vals.reason.value.value?.trim();

  // server-side validation — close the loop right here
  if (env === 'production' && (!reason || reason.length < 10)) {
    return ack({
      response_action: 'errors',
      errors: { reason: 'Production deploys need a real reason (10+ chars).' },
    });
  }

  await ack();                          // accept the submission
  await triggerDeploy({ service, env, reason, user: body.user.id });
  await client.chat.postMessage({
    channel: '#deploys-prod',
    text: `🚀 <@${body.user.id}> deploying *${service}* to *${env}* — ${reason}`,
  });
});

The validation-as-inline-errors pattern is the whole reason to use a modal over command text. The user can’t submit garbage; they get told exactly which field is wrong, in place, and nothing executes until the input is valid.

Multi-step and dynamic modals

Two patterns level this up:

Dynamic options. Don’t hardcode the service list — populate the dropdown from an external_select backed by an endpoint that returns your live service catalog. Now the modal always reflects what’s actually deployable.

Push and update views. A modal can push a second view onto the stack (views.push) for a confirmation step, or update itself in place (views.update) as the user makes choices — for example, revealing a “canary percentage” field only when they pick production. You stack a review screen before anything dangerous happens.

// reveal a confirmation step before a production deploy
await client.views.push({
  trigger_id: body.trigger_id,
  view: confirmationView({ service, env }),
});

Carry context with private_metadata

When you open a modal from a specific message or run, you need to remember what it was about when the submission comes back. Stash it in private_metadata — a string field that round-trips with the view:

view: { ...deployModal(), private_metadata: JSON.stringify({ runId, channel }) }

On submit, parse it back out. This is how you keep modals stateless on your side without a database.

Keep it accessible and honest

Two habits keep interactive tooling trustworthy. First, label destructive submit buttons clearly and back them with a confirmation view — a modal makes it too easy to ship a one-click prod action. Second, always post a channel message after a modal action so there’s a visible audit trail of who did what; modal submissions are otherwise invisible to everyone but the submitter.

Where to take it

Pick your gnarliest slash command — the one with five flags nobody remembers — and turn it into a modal. You’ll get validation, dropdowns, and a confirmation step almost for free, and your team will stop misremembering the syntax. For more on building scannable, actionable Slack ops UIs, see our Slack for ops guides.

Interactive components can trigger real infrastructure actions. Validate server-side, confirm destructive submissions, and log every action for audit.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.