> ## Documentation Index
> Fetch the complete documentation index at: https://deepline.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Slack API Guide for GTM Workflows

> Use channel listing first to confirm conversation IDs, then post/reply or fetch history with least-privilege bot scopes.

## Playbook

# slack\_message\_with\_hitl — Agent Guidance

## What it does

Posts a message to Slack with buttons you define, pauses the workflow, and resumes when a human clicks a button. Use it whenever a workflow needs a human decision before proceeding.

## Mental model

1. You define **what the human sees** (text, optional blocks, + buttons) and **what data each button carries** (emit).
2. The system posts the message, pauses the workflow, and waits.
3. A human clicks a button.
4. The workflow resumes. The next step receives `selected_option` (the button id) and `event_payload` (the button's emit data merged with Slack metadata).

That's it. One input, one pause, one output.

## blocks (Block Kit)

You can provide custom Slack Block Kit blocks to control the message layout. When `blocks` is provided, it replaces the default section block generated from `text`. The system always appends:

* A **status context block** (hourglass / checkmark)
* An **actions block** with your buttons (injected with routing metadata)

**Do NOT include `actions` blocks** in your blocks array — interactive buttons are managed via the `buttons` field.

When `blocks` is provided, `text` is still required — Slack uses it as the notification preview and accessibility fallback.

### Example with blocks

```json theme={null}
{
  "channel": "C0123456789",
  "text": "New lead: Sarah Chen, VP Marketing at Stripe",
  "blocks": [
    {
      "type": "header",
      "text": { "type": "plain_text", "text": "New Lead for Review" }
    },
    {
      "type": "section",
      "fields": [
        { "type": "mrkdwn", "text": "*Name:*\nSarah Chen" },
        { "type": "mrkdwn", "text": "*Title:*\nVP Marketing" },
        { "type": "mrkdwn", "text": "*Company:*\nStripe" },
        { "type": "mrkdwn", "text": "*Email:*\nsarah@stripe.com" }
      ]
    },
    { "type": "divider" }
  ],
  "buttons": [
    {
      "id": "approve",
      "label": "Approve",
      "style": "primary",
      "emit": { "decision": "approved" }
    },
    {
      "id": "reject",
      "label": "Reject",
      "style": "danger",
      "emit": { "decision": "rejected" }
    }
  ],
  "allow_edit": true
}
```

### Supported block types

All Slack Block Kit block types are supported: `section`, `header`, `divider`, `context`, `image`, `rich_text`, `actions` (non-HITL only), `input`, `video`, `table`, `markdown`, `file`, `context_actions`, `task_card`, `plan`.

See the [Block Kit reference](https://docs.slack.dev/reference/block-kit/blocks) for full specifications.

## Button schema

```json theme={null}
{
  "buttons": [
    {
      "id": "hubspot",
      "label": "Push to HubSpot",
      "style": "primary",
      "emit": { "crm": "hubspot", "action": "create_contact" }
    },
    {
      "id": "skip",
      "label": "Skip",
      "style": "danger"
    }
  ]
}
```

Each button has:

| Field   | Required | Description                                                                                   |
| ------- | -------- | --------------------------------------------------------------------------------------------- |
| `id`    | yes      | Unique identifier. Returned as `selected_option` when clicked. Cannot be `"edit"` (reserved). |
| `label` | yes      | Text shown on the button in Slack.                                                            |
| `style` | no       | `"primary"` (green) or `"danger"` (red). Omit for neutral gray.                               |
| `emit`  | no       | Arbitrary key-value data merged into `event_payload` when this button is clicked.             |

Rules:

* 1–5 buttons required.
* Button ids must be unique.
* `"edit"` is a reserved id — use `allow_edit` instead.
* Unknown fields on buttons are rejected (`additionalProperties: false`).

## What the next step receives

When a human clicks a button, the workflow resumes with this output:

```json theme={null}
{
  "resumed": true,
  "timed_out": false,
  "selected_option": "hubspot",
  "event_payload": {
    "crm": "hubspot",
    "action": "create_contact",
    "selected_option": "hubspot",
    "slack_user_id": "U0123456789",
    "slack_channel_id": "C0123456789"
  },
  "final_text": "Lead: Sarah Chen, VP Marketing at Stripe",
  "edited": false,
  "signal": "workflow:run_1:block_2:slack:hitl",
  "actor": {
    "slack_user_id": "U0123456789",
    "slack_channel_id": "C0123456789"
  },
  "message_ref": {
    "channel": "C0123456789",
    "ts": "1712501111.000100"
  }
}
```

* `selected_option` — the `id` of the button that was clicked.
* `event_payload` — the button's `emit` data merged with Slack signal metadata (`selected_option`, `final_text`, `edited`, `slack_user_id`, `slack_channel_id`, etc). If the button has no `emit`, event\_payload only contains the signal metadata. **Merge precedence:** `emit` keys overwrite signal metadata keys on collision — avoid using `selected_option`, `final_text`, `edited`, `slack_user_id`, or `slack_channel_id` as emit keys.
* `final_text` — the message text at the time of the click (may differ from the original if the human used Edit).
* `edited` — whether the human modified the draft text before clicking.

Branch on `selected_option` in the next workflow step to decide what to do.

## allow\_edit

Set `"allow_edit": true` to add an Edit button that opens a Slack modal where the human can modify the `text` field. Editing does NOT resume the workflow — the human must still click one of your defined buttons after editing.

Defaults to `false`. Use it when the `text` field contains a draft the human should be able to revise (e.g., a draft email, a message template).

## post\_interaction

Controls what happens to the Slack message after any button is clicked:

```json theme={null}
{
  "post_interaction": {
    "disable_buttons": true,
    "replace_message": "Decision recorded."
  }
}
```

* `disable_buttons` — remove all buttons from the message after a click (default: true).
* `replace_message` — replace the entire message text with this string.
* `replace_blocks` — replace the entire message with custom Slack blocks (advanced).

This applies to all buttons. There is no per-button post\_interaction.

## Timeout

```json theme={null}
{
  "timeout": "30m",
  "on_timeout": "return_null",
  "timeout_emit": { "reason": "timed_out" }
}
```

* `timeout` — how long to wait for a click (default: 24h). Examples: `"30m"`, `"2h"`, `"24h"`.
* `on_timeout` — `"return_null"` (resume with `timed_out: true`) or `"error"` (fail the workflow step).
* `timeout_emit` — data returned as `event_payload` on timeout.

## Common patterns

### Simple confirmation gate (1 button)

```json theme={null}
{
  "channel": "C0123456789",
  "text": "About to enrich 500 leads. This will cost ~$25.",
  "buttons": [{ "id": "confirm", "label": "Go ahead", "style": "primary" }]
}
```

### Approve / reject with editable draft

```json theme={null}
{
  "channel": "C0123456789",
  "text": "Hi John, we'd love to set up a call to discuss...",
  "buttons": [
    {
      "id": "approve",
      "label": "Send",
      "style": "primary",
      "emit": { "decision": "send" }
    },
    {
      "id": "reject",
      "label": "Drop",
      "style": "danger",
      "emit": { "decision": "drop" }
    }
  ],
  "allow_edit": true
}
```

### Multi-option CRM routing (5 buttons)

```json theme={null}
{
  "channel": "C0123456789",
  "text": "New lead: Sarah Chen, VP Marketing at Stripe. Which CRM?",
  "buttons": [
    {
      "id": "hubspot",
      "label": "HubSpot",
      "style": "primary",
      "emit": { "crm": "hubspot" }
    },
    {
      "id": "salesforce",
      "label": "Salesforce",
      "emit": { "crm": "salesforce" }
    },
    { "id": "pipedrive", "label": "Pipedrive", "emit": { "crm": "pipedrive" } },
    { "id": "attio", "label": "Attio", "emit": { "crm": "attio" } },
    { "id": "skip", "label": "Skip Lead", "style": "danger" }
  ]
}
```

Next step branches: `if selected_option === "skip"` → end; otherwise `event_payload.crm` tells you which API to call.

## Using HITL in a workflow

A Deepline workflow is a `commands` array. Each command has an `alias`, a `tool`, and a `payload`.

**Important — `row` access differs by step type:**

| Step type                        | Access pattern                  | Example                             |
| -------------------------------- | ------------------------------- | ----------------------------------- |
| HITL (`slack_message_with_hitl`) | `row.<alias>.selected_option`   | `row.qualify.selected_option`       |
| JavaScript (`run_javascript`)    | `row.<alias>.result.your_field` | `row.draft_email.result.email_body` |

HITL output is stored **flat** — no `.result` wrapper. JS output is wrapped in `{ result, status, meta }`.

**Template interpolation** in tool payloads uses `{{...}}` mustache syntax, NOT `${...}` JS template literals:

```
"text": "{{row.draft_email.result.email_body}}"     ← correct
"text": "${row.draft_email.result.email_body}"       ← WRONG, won't interpolate
```

Here's a 3-step workflow: qualify a lead via HITL, branch on the decision, then post the draft email for approval.

```json theme={null}
{
  "name": "lead_qualify_and_outbound",
  "publish": true,
  "config": {
    "version": 1,
    "commands": [
      {
        "alias": "qualify",
        "tool": "slack_message_with_hitl",
        "payload": {
          "channel": "C07V9TZ4MSQ",
          "text": "New lead: Sarah Chen, VP Marketing at Stripe.\nEmail: sarah@stripe.com\n\nHow should we classify this lead?",
          "buttons": [
            {
              "id": "hot",
              "label": "Hot Lead",
              "style": "primary",
              "emit": { "qualification": "hot" }
            },
            {
              "id": "warm",
              "label": "Warm Lead",
              "emit": { "qualification": "warm" }
            },
            { "id": "disqualify", "label": "Disqualify", "style": "danger" }
          ],
          "timeout": "2h",
          "on_timeout": "return_null"
        }
      },
      {
        "alias": "draft_email",
        "tool": "run_javascript",
        "payload": {
          "code": "const q = row.qualify; if (q.timed_out || q.selected_option === 'disqualify') return { skip: true }; const isHot = q.selected_option === 'hot'; const body = isHot ? 'Hi Sarah, I\\'d love to set up a quick demo call this week...' : 'Hi Sarah, we published a playbook you might find useful...'; return { skip: false, email_body: body, qualification: q.selected_option };"
        }
      },
      {
        "alias": "approve_email",
        "tool": "slack_message_with_hitl",
        "payload": {
          "channel": "C07V9TZ4MSQ",
          "text": "{{row.draft_email.result.email_body}}",
          "buttons": [
            {
              "id": "send",
              "label": "Approve & Send",
              "style": "primary",
              "emit": { "decision": "send" }
            },
            {
              "id": "drop",
              "label": "Drop",
              "style": "danger",
              "emit": { "decision": "drop" }
            }
          ],
          "allow_edit": true,
          "timeout": "4h",
          "on_timeout": "return_null"
        }
      }
    ]
  }
}
```

### How data flows between steps

```
Step 1 (qualify)                    Step 2 (draft_email)                Step 3 (approve_email)
┌──────────────────┐               ┌──────────────────┐               ┌──────────────────┐
│ HITL: 3 buttons  │               │ JS: reads Step 1 │               │ HITL: approve or │
│                  │──output──────▶│ via row.qualify   │──output──────▶│ drop the email   │
│ Waits for human  │               │ (flat, no .result)│               │ allow_edit: true  │
└──────────────────┘               └──────────────────┘               └──────────────────┘
```

* **Step 1** posts to Slack with 3 buttons, pauses, resumes when human clicks.
* **Step 2** is a `run_javascript` step. It reads Step 1's HITL output at `row.qualify.selected_option` (flat — no `.result`). If the human disqualified the lead or it timed out, it returns `{ skip: true }`. Otherwise it constructs an email draft.
* **Step 3** is another HITL step. The `text` field uses `{{row.draft_email.result.email_body}}` (mustache template, `.result` because it's reading a JS step). `allow_edit: true` lets the human revise it. After approval, `row.approve_email.final_text` contains the final email body.

### Key patterns

**Reading prior HITL output:** `row.<alias>.selected_option` gives you the button ID. `row.<alias>.event_payload` gives you the button's emit data. `row.<alias>.final_text` gives you the (possibly edited) text. No `.result` wrapper.

**Reading prior JS output:** `row.<alias>.result.your_field` — JS steps wrap output in `{ result, status, meta }`.

**Template interpolation in payloads:** Use `{{row.<alias>.field}}` mustache syntax. For JS outputs: `{{row.<alias>.result.field}}`. For HITL outputs: `{{row.<alias>.selected_option}}`.

**Branching:** Use a `run_javascript` step between HITL steps to inspect the prior result and decide what to do next. Return a flag like `{ skip: true }` that downstream steps can check.

**Passing data forward:** You don't need to stuff lead data into every button's `emit`. The workflow engine carries all step outputs in `row`. Any step can read any prior step's output via the patterns above.
