Skip to main content

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

Prerequisite: You must first enable webhooks for your organization. See the Webhooks Quickstart to get started.
1

Open the Webhooks tab

Go to the Webhooks tab in the Cyberdesk dashboard.
2

Add a new endpoint

Click ”+ Add Endpoint” to create a new webhook endpoint.
3

Select Slack as the destination

In the “New Endpoint” page, click the “Webhook” dropdown (with a down chevron) and select “Slack”.
4

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.
5

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
The transformation supports Slack Block Kit syntax, giving you full control over message formatting with headers, sections, buttons, and more.
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 syntax for rich formatting
This example uses Slack Block Kit to create rich messages with status, timestamps, duration, errors, and a button linking to the run:
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;
}
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:
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;
}
6

Create the integration

Click “Create” to complete the configuration.

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.
You can create multiple Slack integrations pointing to different channels — for example, one channel for production runs and another for development.