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

# Slack Integration

> Get notified in Slack when your runs complete

## Overview

Cyberdesk has a native integration with Slack. When enabled, you'll receive Slack messages whenever a run completes, showing:

* Run status (success, error, or cancelled)
* Date and time information
* A direct link to the run in the Cyberdesk dashboard
* Key run details

This is a great way to stay informed about your automation runs without constantly checking the dashboard.

## Setup

<Note>
  **Prerequisite:** You must first enable webhooks for your organization. See the [Webhooks Quickstart](/webhooks/quickstart) to get started.
</Note>

<Steps>
  <Step title="Open the Webhooks tab">
    Go to the **Webhooks** tab in the Cyberdesk dashboard.
  </Step>

  <Step title="Add a new endpoint">
    Click **"+ Add Endpoint"** to create a new webhook endpoint.
  </Step>

  <Step title="Select Slack as the destination">
    In the "New Endpoint" page, click the **"Webhook"** dropdown (with a down chevron) and select **"Slack"**.
  </Step>

  <Step title="Connect to Slack">
    Click **"Connect to Slack"**. This will redirect you to Slack where you can select which Slack workspace and channel you'd like to receive messages. Once you're done, click **"Allow"**, and you'll be brought back to Cyberdesk.
  </Step>

  <Step title="Customize the transformation (optional)">
    You can edit the **transformation code** to customize how messages appear in Slack. The transformation has access to all fields in the run (`webhook.payload.run`), so you can:

    * Filter to only send messages for certain statuses (e.g., skip `cancelled` runs)
    * Customize the message format
    * Include specific fields from `input_values` or `output_data`

    <Tip>
      The transformation supports [Slack Block Kit](https://api.slack.com/block-kit) syntax, giving you full control over message formatting with headers, sections, buttons, and more.
    </Tip>

    <Accordion title="Customize with AI">
      Copy the example transformation code below and paste it into ChatGPT, Claude, or another LLM along with your request. For example:

      > "Modify this code to only send Slack messages for errored and cancelled runs, and include the workflow\_id in the title."

      **Give the LLM this context:**

      * The run object is accessed via `webhook.payload.run`
      * Available fields: `id`, `workflow_id`, `session_id`, `status` (`success`, `error`, `cancelled`), `error` (array of strings), `input_values`, `output_data`, `created_at`, `started_at`, `ended_at`, `release_session_after`
      * Set `webhook.cancel = true` to skip sending the message
      * The payload supports [Slack Block Kit](https://api.slack.com/block-kit) syntax for rich formatting
    </Accordion>

    <Accordion title="Example transformation code">
      This example uses Slack Block Kit to create rich messages with status, timestamps, duration, errors, and a button linking to the run:

      ```javascript theme={null}
      function handler(webhook) {
        if (webhook.eventType === 'run_complete') {
          const run = webhook.payload.run;
          
          // Filter out cancelled runs
          if (run.status === 'cancelled') {
            webhook.cancel = true;
            return webhook;
          }
          
          // Normalize "session closed" flag
          const isSessionClosed =
            run.status === 'success' &&
            (run.release_session_after === true || run.release_session_after === 'true');
          
          // Base title strings
          let baseTitle;
          if (run.status === 'success') {
            if (isSessionClosed) {
              baseTitle = "Cyberdesk - Run Successful + Session Closed!";
            } else {
              baseTitle = "Cyberdesk - Run Successful!";
            }
          } else if (run.status === 'error') {
            baseTitle = "Cyberdesk - Run Error!";
          } else {
            baseTitle = "Cyberdesk - Run Notification";
          }
          
          // Add emoji prefix based on status
          let titleText;
          if (run.status === 'success') {
            titleText = `🚀 ${baseTitle}`;
          } else if (run.status === 'error') {
            titleText = `🔥 ${baseTitle}`;
          } else {
            titleText = `🖥️ ${baseTitle}`;
          }
          
          const statusEmoji = {
            scheduling: ':hourglass:',
            running: ':runner:',
            success: ':white_check_mark:',
            error: ':bangbang:'
          };
          
          const createdAt = run.created_at ? new Date(run.created_at) : null;
          const startedAt = run.started_at ? new Date(run.started_at) : null;
          const endedAt   = run.ended_at   ? new Date(run.ended_at)   : null;
          
          function formatDuration(ms) {
            let seconds = Math.floor(ms / 1000);
            const hrs = Math.floor(seconds / 3600);
            seconds -= hrs * 3600;
            const mins = Math.floor(seconds / 60);
            seconds -= mins * 60;
            const parts = [];
            if (hrs > 0) parts.push(`${hrs} ${hrs === 1 ? 'hr' : 'hrs'}`);
            if (mins > 0) parts.push(`${mins} ${mins === 1 ? 'min' : 'mins'}`);
            parts.push(`${seconds} ${seconds === 1 ? 'sec' : 'secs'}`);
            return parts.join(' ');
          }
          
          let duration = "";
          if (startedAt && endedAt) {
            duration = formatDuration(endedAt - startedAt);
          }
          
          // Build error blocks if any
          let errorBlock = [];
          if (Array.isArray(run.error) && run.error.length > 0) {
            errorBlock = [
              {
                type: "section",
                text: {
                  type: "mrkdwn",
                  text: "*:warning: Errors:*"
                }
              },
              {
                type: "section",
                text: {
                  type: "mrkdwn",
                  text: run.error.map(e => `• :x: \`${e}\``).join("\n")
                }
              },
              { type: "divider" }
            ];
          }
          
          // Main metadata fields
          const metaFields = [
            {
              type: "mrkdwn",
              text: `*Run ID:*\n\`${run.id}\``
            },
            {
              type: "mrkdwn",
              text: `*Workflow ID:*\n\`${run.workflow_id}\``
            },
            {
              type: "mrkdwn",
              text: `*Status:*\n${statusEmoji[run.status] || ':grey_question:'} \`${run.status}\``
            },
            {
              type: "mrkdwn",
              text: `*Session Closed:*\n\`${isSessionClosed ? 'yes' : 'no'}\``
            }
          ];
          
          if (run.session_id) {
            metaFields.push({
              type: "mrkdwn",
              text: `*Session ID:*\n\`${run.session_id}\``
            });
          }
          
          webhook.payload = {
            blocks: [
              // Title
              {
                type: "header",
                text: {
                  type: "plain_text",
                  text: titleText,
                  emoji: true
                }
              },
              // Status + IDs + session info
              {
                type: "section",
                fields: metaFields
              },
              { type: "divider" },
              // Timing
              {
                type: "section",
                fields: [
                  {
                    type: "mrkdwn",
                    text: `*Created At:*\n${createdAt ? createdAt.toLocaleString() : "N/A"}`
                  },
                  {
                    type: "mrkdwn",
                    text: `*Started At:*\n${startedAt ? startedAt.toLocaleString() : "N/A"}`
                  },
                  {
                    type: "mrkdwn",
                    text: `*Ended At:*\n${endedAt ? endedAt.toLocaleString() : "N/A"}`
                  },
                  {
                    type: "mrkdwn",
                    text: `*Duration:*\n\`${duration || "N/A"}\``
                  }
                ]
              },
              { type: "divider" },
              // Errors (if any)
              ...errorBlock,
              // Link button
              {
                type: "actions",
                elements: [
                  {
                    type: "button",
                    text: {
                      type: "plain_text",
                      text: "View Run in Cyberdesk"
                    },
                    url: `https://cyberdesk.io/dashboard/runs/${run.id}`
                  }
                ]
              }
            ]
          };
        }
        return webhook;
      }
      ```
    </Accordion>

    <Accordion title="Example: Only notify on errors">
      To only receive Slack messages when runs fail, use this transformation. It creates a rich error alert with all error details, timing information, and a direct link to investigate:

      ```javascript theme={null}
      function handler(webhook) {
        if (webhook.eventType === 'run_complete') {
          const run = webhook.payload.run;
          
          // Only notify on errors - skip success and cancelled runs
          if (run.status !== 'error') {
            webhook.cancel = true;
            return webhook;
          }
          
          // Parse timestamps
          const createdAt = run.created_at ? new Date(run.created_at) : null;
          const startedAt = run.started_at ? new Date(run.started_at) : null;
          const endedAt   = run.ended_at   ? new Date(run.ended_at)   : null;
          
          function formatDuration(ms) {
            let seconds = Math.floor(ms / 1000);
            const hrs = Math.floor(seconds / 3600);
            seconds -= hrs * 3600;
            const mins = Math.floor(seconds / 60);
            seconds -= mins * 60;
            const parts = [];
            if (hrs > 0) parts.push(`${hrs} ${hrs === 1 ? 'hr' : 'hrs'}`);
            if (mins > 0) parts.push(`${mins} ${mins === 1 ? 'min' : 'mins'}`);
            parts.push(`${seconds} ${seconds === 1 ? 'sec' : 'secs'}`);
            return parts.join(' ');
          }
          
          let duration = "N/A";
          if (startedAt && endedAt) {
            duration = formatDuration(endedAt - startedAt);
          }
          
          // Build error list
          let errorBlocks = [];
          if (Array.isArray(run.error) && run.error.length > 0) {
            errorBlocks = [
              {
                type: "section",
                text: {
                  type: "mrkdwn",
                  text: "*:rotating_light: Error Details:*"
                }
              },
              {
                type: "section",
                text: {
                  type: "mrkdwn",
                  text: run.error.map((e, i) => `${i + 1}. \`${e}\``).join("\n")
                }
              }
            ];
          } else {
            errorBlocks = [
              {
                type: "section",
                text: {
                  type: "mrkdwn",
                  text: "*:rotating_light: Error Details:*\n_No error message available_"
                }
              }
            ];
          }
          
          // Build metadata fields
          const metaFields = [
            {
              type: "mrkdwn",
              text: `*Run ID:*\n\`${run.id}\``
            },
            {
              type: "mrkdwn",
              text: `*Workflow ID:*\n\`${run.workflow_id}\``
            }
          ];
          
          if (run.session_id) {
            metaFields.push({
              type: "mrkdwn",
              text: `*Session ID:*\n\`${run.session_id}\``
            });
          }
          
          metaFields.push({
            type: "mrkdwn",
            text: `*Duration:*\n\`${duration}\``
          });
          
          webhook.payload = {
            blocks: [
              // Alert header
              {
                type: "header",
                text: {
                  type: "plain_text",
                  text: "🔥 Cyberdesk - Run Failed!",
                  emoji: true
                }
              },
              // Context line with timestamp
              {
                type: "context",
                elements: [
                  {
                    type: "mrkdwn",
                    text: `Failed at ${endedAt ? endedAt.toLocaleString() : 'unknown time'}`
                  }
                ]
              },
              { type: "divider" },
              // Error details
              ...errorBlocks,
              { type: "divider" },
              // Metadata
              {
                type: "section",
                fields: metaFields
              },
              { type: "divider" },
              // Action buttons
              {
                type: "actions",
                elements: [
                  {
                    type: "button",
                    text: {
                      type: "plain_text",
                      text: "🔍 Investigate Run"
                    },
                    style: "danger",
                    url: `https://cyberdesk.io/dashboard/runs/${run.id}`
                  }
                ]
              }
            ]
          };
        }
        return webhook;
      }
      ```
    </Accordion>
  </Step>

  <Step title="Create the integration">
    Click **"Create"** to complete the configuration.
  </Step>
</Steps>

## You're done!

You will now receive Slack messages whenever a run completes. The messages include a direct link to view the full run details in the Cyberdesk dashboard.

<Tip>
  You can create multiple Slack integrations pointing to different channels — for example, one channel for production runs and another for development.
</Tip>
