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

Exposing App Actions to Slack Workflow Builder: Designing the Variable Contract

Custom Workflow Builder steps live or die by their input/output variable contract. Learn to type variables, validate untrusted inputs, and version the contract without breaking workflows.

  • #slack
  • #ai
  • #workflow-builder
  • #custom-steps
  • #automation

The first custom Workflow Builder step I shipped worked beautifully for two months. Then I “cleaned up” the function definition, renamed an output from ticket_url to url, and shipped it. Within an hour, four workflows that support had built — workflows I didn’t know existed — were silently passing empty strings into their next steps, because the variable they’d mapped no longer existed. Nobody got an error. The automations just quietly stopped working, and I learned that the code running my step was never the fragile part. The variable contract was.

This is a guide to designing that contract well: the typed input and output variables that let non-engineers wire your app’s action into Workflow Builder’s no-code canvas. When you expose a step via Steps from Apps or the next-gen platform’s custom functions, you’re publishing an API to people who can’t read your code and will assemble it in ways you didn’t anticipate. Treat the contract accordingly.

The contract is the product

Inside Workflow Builder, your step appears as a block with input fields someone fills in and output variables they can map into later steps. Everything a builder can see and use is the variable contract — and unlike a normal API, your consumers are support leads and engineering managers clicking through a UI, not developers reading docs.

That changes your priorities. The human-readable name and description on each variable aren’t documentation; they’re the entire interface. “Channel” with a description of “Channel to post the result to” is usable. “ch_id” with no description is not. If you’re automating ops with Workflow Builder, the quality of these labels determines whether non-engineers can actually self-serve or whether every workflow runs through you anyway.

Typing inputs so the UI helps builders

Each input variable has a type, and the type determines how Workflow Builder lets a builder supply it. Slack-native types render visual pickers; generic text renders a free-text box that invites typos. Prefer native types wherever you can:

import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";

export const CreateTicket = DefineFunction({
  callback_id: "create_ticket",
  title: "Create incident ticket",
  source_file: "functions/create_ticket.ts",
  input_parameters: {
    properties: {
      reporter: {
        type: Schema.slack.types.user_id,   // visual user picker, not free text
        description: "Who is reporting this incident",
      },
      severity: {
        type: Schema.types.string,
        enum: ["sev1", "sev2", "sev3"],       // constrained dropdown
        description: "Incident severity",
      },
      summary: {
        type: Schema.types.string,
        description: "One-line summary of the incident",
      },
    },
    required: ["reporter", "severity", "summary"],
  },
  output_parameters: {
    properties: {
      ticket_url: {
        type: Schema.types.string,
        description: "Link to the created ticket",
      },
    },
    required: ["ticket_url"],
  },
});

The enum on severity turns a typo-prone text box into a dropdown. The slack.types.user_id gives builders a person picker instead of asking them to paste a user ID. Every constraint you encode in the type is a class of bad input the builder can’t supply in the first place.

Outputs must be declared, and never trusted to be optional later

Notice that ticket_url is declared in output_parameters. This is mandatory — Workflow Builder can only let downstream steps map outputs you declare up front. You cannot return an ad-hoc key from your handler and expect it to appear as a mappable variable; if it isn’t in the contract, it doesn’t exist as far as the canvas is concerned.

This is also where my renaming disaster lived. Once a builder maps ticket_url into a downstream step, that name is load-bearing. The rules that keep you out of trouble:

  • Add, never rename. Need a better name? Add the new output alongside the old one and deprecate the old one over time.
  • Never remove an output in use. Removing it breaks every workflow that mapped it, silently.
  • Add new inputs as optional. A new required input breaks existing workflows that don’t supply it.

Treat the contract like a public API with backward-compatibility guarantees, because to the people building workflows, that’s exactly what it is.

The no-code mapping is not a trust boundary

Workflow Builder’s visual mapping creates a dangerous illusion of safety. A builder wires an upstream value into your summary input by clicking, and it feels validated. It isn’t. That upstream value could be anything — another step’s raw output, a free-text field someone typed into, a variable from a trigger you’ve never seen. Your handler must validate every input as untrusted:

export default SlackFunction(CreateTicket, async ({ inputs, complete, fail }) => {
  const { reporter, severity, summary } = inputs;

  if (!summary || summary.length > 500) {
    return await fail({ error: "Summary must be 1–500 characters." });
  }
  if (!["sev1", "sev2", "sev3"].includes(severity)) {
    return await fail({ error: `Invalid severity: ${severity}` });
  }

  const ticket = await createTicket({ reporter, severity, summary });
  return await complete({ outputs: { ticket_url: ticket.url } });
});

The fail path matters as much as the complete path. When your step fails, the builder sees the message you pass — so make it actionable. A step that throws an unhandled error leaves the workflow in a confusing state; a step that calls fail with “Summary must be 1–500 characters” tells the builder exactly what to fix.

Letting AI draft the definition, with a human on the contract

An LLM is genuinely useful for scaffolding the function definition and handler — the structure is boilerplate-heavy and well-represented in training data. A prompt like “write a Slack custom function definition for creating an incident ticket with reporter (user), severity (enum), and summary inputs, returning a ticket URL” produces a solid first draft.

Generate a deno-slack-sdk custom function for Workflow Builder that creates an incident ticket. Inputs: reporter as a Slack user_id, severity as an enum of sev1/sev2/sev3, summary as a string. Output: ticket_url. Validate inputs in the handler and call fail with a clear message on bad input.

What the AI won’t do unprompted is enforce the versioning discipline — it’ll happily suggest renaming an output to something cleaner, because it has no idea four live workflows depend on the old name. That judgment is yours. The AI drafts the contract; the human guards its backward compatibility. Keep your refined versions in a prompt library so the next step you expose starts from the same disciplined template, and pair it with the related custom function SDK prompt for the runtime details.

Wrapping Up

A custom Workflow Builder step is an API for non-engineers, and its variable contract is the whole interface. Type your inputs so the UI guides builders toward valid values, declare every output up front, validate everything in the handler because the no-code mapping guarantees you nothing, and — above all — evolve the contract additively, because the people who built workflows on it won’t see your breaking change until their automation has been quietly broken for an hour. Let AI scaffold the definition, but keep a human on the versioning, because that’s the part that turns a working integration into one that stays working.

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.