SR Connect script failure notifier in Slack


Intro video is not displayed because you have disallowed functional cookies.
Get Started

Not the template you're looking for? Browse more.

About the template


This template automatically monitors script execution failures in ScriptRunner Connect and sends detailed notifications to a Slack channel. Get started to learn more.

About ScriptRunner Connect


What is ScriptRunner Connect?

ScriptRunner Connect is an AI assisted code-first (JavaScript/TypeScript) integration platform (iPaaS) for building complex integrations and automations.

Can I try it out for free?

Yes. ScriptRunner Connect comes with a forever free tier.

Can I customize the integration logic?

Absolutely. The main value proposition of ScriptRunner Connect is that you'll get full access to the code that is powering the integration, which means you can make any changes to the the integration logic yourself.

Can I change the integration to communicate with additional apps?

Yes. Since ScriptRunner Connect specializes in enabling complex integrations, you can easily change the integration logic to connect to as many additional apps as you need, no limitations.

What if I don't feel comfortable making changes to the code?

First you can try out our AI assistant which can help you understand what the code does, and also help you make changes to the code. Alternatively you can hire our professionals to make the changes you need or build new integrations from scratch.

Do I have to host it myself?

No. ScriptRunner Connect is a fully managed SaaS (Software-as-a-Service) product.

What about security?

ScriptRunner Connect is ISO 27001 and SOC 2 certified. Learn more about our security.

Template Content


README

Scripts

TypeScriptCheckFailedNotifications
Scheduled

README


SR Connect script failure notifier in Slack

📋 Overview

This integration automatically monitors script execution failures in ScriptRunner Connect and sends detailed notifications to a Slack channel. The integration runs on a scheduled basis, checking for failed script invocations since the last execution and sending comprehensive error reports including invocation details, error messages, and stack traces.

Here is a demo of how that template was built. This template was developed with the assistance of an AI agent.

Key Features:

  • Automatic monitoring of script execution failures
  • Tracks failed invocations with statuses: TIMED_OUT, FUNCTION_ERROR, RUNTIME_ERROR, MALFORMED_PAYLOAD_ERROR
  • Sends detailed Slack notifications with error information
  • Includes direct links to console logs and HTTP logs in ScriptRunner Connect
  • Processes multiple failures concurrently with configurable throttling
  • Prevents duplicate notifications by tracking last execution time

🖊️ Setup

Connectors

Slack Connector

  • Create a Slack Connector in ScriptRunner Connect web UI
  • Configure with appropriate Slack authentication credentials

Generic Connector (for ScriptRunner Connect REST API)

  • Create a Generic Connector in ScriptRunner Connect web UI
  • Configure with ScriptRunner Connect REST API base URL and authentication

API Connections

Slack API Connection

  • Create a Slack API Connection in ScriptRunner Connect web UI
  • Link to the Slack Connector created above
  • This connection is used to send notifications to Slack channels. Make sure your bot is invited to the channel you want to send notifications into.

ScriptRunner Connect API Connection

  • Create a Generic API Connection in ScriptRunner Connect web UI
  • Link to the Generic Connector created above
  • Configure with the ScriptRunner Connect REST API endpoint
  • This connection is used to retrieve invocation logs and console logs

Scheduled Triggers

CheckFailedNotifications Trigger

  • Create a Scheduled Trigger in ScriptRunner Connect web UI
  • Link to the CheckFailedNotifications script
  • Recommended Schedule: Every 15 minutes
    • UI Schedule Builder: Set interval to 15 minutes
    • CRON Format: 0 0/15 * * * ? (runs every 15 minutes at the top of the hour)
  • Timezone: UTC (all schedules use UTC timezone)

Parameters

Configure the following Parameters in the ScriptRunner Connect web UI Parameters section:

Parameter NameTypeDescription
TEAM_IDTextTeam ID where the workspace is located
WORKSPACE_IDTextWorkspace ID to filter logs for
ENVIRONMENT_NAMETextEnvironment name to filter logs for
SLACK_CHANNELTextSlack channel where to send notifications
CONCURRENCYNumberHow many concurrent jobs to maintain when processing failures

Finding Parameter Values:

  • TEAM_ID: Can be found in Team settings > General section in the ScriptRunner Connect web UI
  • WORKSPACE_ID: Can be found in the workspace URL. For example, in the URL ./workspace/01K9EZWJW0C5P6R26V5WW4CV7D/environment/01K9EZWJWZP8VF434KBR7QVZWP, the workspace ID is 01K9EZWJW0C5P6R26V5WW4CV7D (the segment after /workspace/)

Security Considerations:

  • TEAM_ID and WORKSPACE_ID are workspace identifiers and can be stored as Text type
  • SLACK_CHANNEL should be the channel name or ID (e.g., #channel-name or C1234567890)
  • CONCURRENCY controls how many failed invocations are processed simultaneously - adjust based on your workspace's rate limits and performance requirements

🚀 Using the Integration

Once configured, the integration runs automatically on the scheduled interval. No manual intervention is required.

How It Works

  1. Scheduled Execution: The script runs every 15 minutes (or your configured interval)
  2. Time Range Detection: Retrieves failed invocations since the last successful execution
  3. Failure Detection: Filters for invocations with execution statuses:
    • TIMED_OUT: Script execution exceeded time limit
    • FUNCTION_ERROR: Error occurred during script execution
    • RUNTIME_ERROR: Runtime error during execution
    • MALFORMED_PAYLOAD_ERROR: Invalid payload received
  4. Error Extraction: Downloads console logs for each failed invocation to extract error messages and stack traces
  5. Notification Sending: Sends formatted Slack messages with:
    • Invocation ID and timestamp
    • Trigger type and execution duration
    • Links to console logs and HTTP logs (if available)
    • Failure reason
    • Error details with stack trace (if available)

Testing

Manual Trigger:

  • Navigate to the CheckFailedNotifications script in the ScriptRunner Connect web UI
  • Click the play button in the Resource Manager tree or in the code editor header
  • Check the Slack channel for notifications

Verification:

  • Review Script Invocation Logs in ScriptRunner Connect web UI to see execution details
  • Check Slack channel for notification messages
  • Verify that failed invocations are being detected and reported correctly

Expected Output

Slack notifications will appear in the configured channel with the following format:

Script Execution Failed

Invocation ID: `inv-123456`
Timestamp: 2025-01-15 10:30:45 UTC
Trigger Type: Scheduled Trigger
Execution Duration: 2m 15s
Console Logs: <link|5>
HTTP Logs: <link|3>
Failure Reason: FUNCTION_ERROR

If error details are available, an additional attachment will include:

  • Error message (e.g., "Error: Cannot read property 'x' of undefined")
  • Stack trace with file locations and line numbers

❗️ Considerations

Rate Limiting:

  • The integration uses concurrent processing with throttling to respect API rate limits
  • Adjust the CONCURRENCY parameter if you experience rate limiting issues
  • ScriptRunner Connect REST API may have rate limits - monitor HTTP logs for 429 responses

Time Range Handling:

  • On first execution, the integration checks the past hour for failures
  • Subsequent executions check from the last successful execution time
  • If the script fails to complete, the next execution will check from the last saved timestamp, potentially missing some failures

Memory and Performance:

  • Console logs are downloaded for each failed invocation, which may impact performance with many failures
  • Large console logs may increase processing time
  • Consider adjusting CONCURRENCY if processing many failures simultaneously

Notification Frequency:

  • Each failed invocation generates one Slack notification
  • Multiple failures in a short time period will result in multiple notifications
  • Consider Slack rate limits if processing many failures

Record Storage:

  • The integration uses Record Storage to track last execution time
  • Stored at environment scope (default)
  • Key: last-execution-time
  • If Record Storage is cleared, the integration will check the past hour on next execution

🔧 Troubleshooting

No Notifications Received:

  • Verify the Scheduled Trigger is enabled and running
  • Check Script Invocation Logs for execution errors
  • Verify SLACK_CHANNEL parameter is correctly configured (use channel name with # or channel ID)
  • Ensure Slack API Connection is properly configured and authenticated
  • Check Slack channel permissions - the bot must have permission to post messages

Missing Failures:

  • Review Script Invocation Logs to verify the script is executing
  • Check that WORKSPACE_ID and ENVIRONMENT_NAME parameters match your workspace configuration
  • Verify the time range - check Record Storage value for last-execution-time to see what time period is being checked
  • Ensure failed invocations match the filtered execution statuses

API Errors:

  • Check HTTP Logs in ScriptRunner Connect web UI for API call failures
  • Verify Generic Connector is configured correctly for ScriptRunner Connect REST API

Performance Issues:

  • Reduce CONCURRENCY parameter if experiencing timeouts or rate limits
  • Check Script Invocation Logs for execution duration - script has 15-minute maximum execution time
  • Monitor console log sizes - very large logs may slow processing

Error Details Not Appearing:

  • Console logs may not be available for all failure types
  • Some errors may not have extractable error details
  • Check Script Invocation Logs to verify console logs are being downloaded successfully

API Connections


TypeScriptCheckFailedNotifications

import { getInvocationLogs, getInvocationConsoleLogs, InvocationLog } from './SRConnectApi';
import Slack from './api/slack';
import { throttleAll } from 'promise-throttle-all';
import { RecordStorage } from '@sr-connect/record-storage';

/**
 * Test script to retrieve and display invocation logs for today
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default async function (event: any, context: Context<EV>): Promise<void> {
    // Get required parameters from environment variables
    const teamId = context.environment.vars.TEAM_ID;
    const workspaceId = context.environment.vars.WORKSPACE_ID;
    const environmentName = context.environment.vars.ENVIRONMENT_NAME;
    const slackChannel = context.environment.vars.SLACK_CHANNEL;
    const concurrency = context.environment.vars.CONCURRENCY;

    // Validate required environment variables
    if (!teamId) {
        throw new Error('TEAM_ID environment variable is required but not defined');
    }
    if (!workspaceId) {
        throw new Error('WORKSPACE_ID environment variable is required but not defined');
    }
    if (!environmentName) {
        throw new Error('ENVIRONMENT_NAME environment variable is required but not defined');
    }
    if (!slackChannel) {
        throw new Error('SLACK_CHANNEL environment variable is required but not defined');
    }
    if (!concurrency) {
        throw new Error('CONCURRENCY environment variable is required but not defined');
    }

    // Initialize Record Storage to track last execution time
    const storage = new RecordStorage();
    const LAST_EXECUTION_TIME_KEY = 'last-execution-time';

    // Get current time (this will be stored for next execution)
    const currentTime = new Date();
    const now = currentTime.toISOString();

    // Get last execution time from Record Storage
    const lastExecutionTime = await storage.getValue<string>(LAST_EXECUTION_TIME_KEY);

    let startTime: string;
    if (lastExecutionTime) {
        // Use last execution time as start time
        startTime = lastExecutionTime;
    } else {
        // No previous execution found, fetch logs for the past hour
        const oneHourAgo = new Date(currentTime.getTime() - 60 * 60 * 1000);
        startTime = oneHourAgo.toISOString();
    }

    // Store current time for next execution BEFORE processing logs
    // This ensures the timestamp is saved even if the script times out
    await storage.setValue(LAST_EXECUTION_TIME_KEY, now);

    // Log time period
    console.log(`Time period: ${formatTimestamp(startTime)} to ${formatTimestamp(now)}`);

    try {
        const failureLogs = await getInvocationLogs({
            teamId,
            workspaceId,
            environmentName,
            startTime,
            to: now,
            executionStatuses: ['TIMED_OUT', 'FUNCTION_ERROR', 'RUNTIME_ERROR', 'MALFORMED_PAYLOAD_ERROR'],
        });

        console.log(`Found ${failureLogs.length} failed invocation(s)`);

        if (failureLogs.length > 0) {
            // Process all invocations concurrently with throttling
            await throttleAll(
                concurrency,
                failureLogs.map((log) => () => processFailedInvocation(log, slackChannel)),
            );
        }
    } catch (error) {
        console.error('Error fetching invocation logs:', error);
        throw error;
    }
}

/**
 * Processes a single failed invocation: downloads logs, extracts error, and sends Slack notification
 */
async function processFailedInvocation(log: InvocationLog, slackChannel: string): Promise<void> {
    try {
        let errorDetails: ErrorDetails | null = null;

        // Download console logs to extract error details
        const consoleLogs = await getInvocationConsoleLogs({
            workspaceId: log.workspace.id,
            invocationId: log.invocationId,
        });

        if (consoleLogs !== null) {
            errorDetails = extractErrorDetails(consoleLogs);
        }

        // Send Slack notification
        await sendSlackNotification(log, errorDetails, slackChannel);
        console.log('Failed invocation:', {
            invocationId: log.invocationId,
            timestamp: formatTimestamp(log.startTime),
            triggerType: log.triggerType,
            executionDuration: formatDuration(log.executionDuration),
            consoleLogs: log.consoleLogsCount,
            httpLogs: log.httpLogsCount,
        });
    } catch (error) {
        console.error(`Failed to process invocation ${log.invocationId}:`, error);
    }
}

/**
 * Sends a Slack notification for a failed invocation
 */
async function sendSlackNotification(
    log: InvocationLog,
    errorDetails: ErrorDetails | null,
    slackChannel: string,
): Promise<void> {
    const workspaceId = log.workspace.id;
    const invocationId = log.invocationId;

    // Build links to logs
    const consoleLogsUrl = `https://app.eu.scriptrunnerconnect.com/invocationlogs/workspace/${workspaceId}/invocation/${invocationId}`;
    const httpLogsUrl = `https://app.eu.scriptrunnerconnect.com/httplogs/workspace/${workspaceId}/invocation/${invocationId}`;

    // Format console logs and HTTP logs with links if count > 0
    const consoleLogsText =
        log.consoleLogsCount > 0 ? `<${consoleLogsUrl}|${log.consoleLogsCount}>` : String(log.consoleLogsCount);
    const httpLogsText = log.httpLogsCount > 0 ? `<${httpLogsUrl}|${log.httpLogsCount}>` : String(log.httpLogsCount);

    // Format the main message
    const messageText = `*Script Execution Failed*

*Invocation ID:* \`${invocationId}\`
*Timestamp:* ${formatTimestamp(log.startTime)}
*Trigger Type:* ${log.triggerType}
*Execution Duration:* ${formatDuration(log.executionDuration)}
*Console Logs:* ${consoleLogsText}
*HTTP Logs:* ${httpLogsText}
*Failure Reason:* ${log.executionStatus}`;

    // Build attachments array
    const attachments: Array<{
        color: string;
        title: string;
        text: string;
        mrkdwn_in?: string[];
    }> = [];

    // Add error details as attachment if available
    if (errorDetails) {
        // Combine error message and stack trace in a single code block
        let attachmentText = errorDetails.errorMessage;

        if (errorDetails.stackTrace) {
            attachmentText += `\n${errorDetails.stackTrace}`;
        }

        attachments.push({
            color: 'danger',
            title: 'Error Details',
            text: `\`\`\`\n${attachmentText}\n\`\`\``,
            mrkdwn_in: ['text'],
        });
    }

    // Send message to Slack
    await Slack.Chat.postMessage({
        body: {
            channel: slackChannel,
            text: messageText,
            attachments: attachments.length > 0 ? attachments : undefined,
            unfurl_links: false,
        },
    });
}

/**
 * Extracts error details from console logs
 * @returns Error details if found, null otherwise
 */
function extractErrorDetails(consoleLogs: unknown): ErrorDetails | null {
    const logs = consoleLogs as ConsoleLogsResponse;

    // Find the SYSTEM_LOG entry with "Error while invoking default function:"
    const errorLog = logs.invocationLogs?.find(
        (entry) =>
            entry.type === 'SYSTEM_LOG' &&
            entry.args?.[0]?.values?.[0]?.value === 'Error while invoking default function:',
    );

    if (!errorLog || !errorLog.args?.[1] || !errorLog.args[1].thrown) {
        return null;
    }

    const errorArg = errorLog.args[1];
    const errorValueEntry = errorArg.values?.[0];

    if (!errorValueEntry) {
        return null;
    }

    // Check if error is an Error object (type === 'Error')
    if (errorValueEntry.type === 'Error' && errorValueEntry.value && typeof errorValueEntry.value === 'object') {
        const error = errorValueEntry.value as ErrorObject;

        let errorMessage = `${error.name}: ${error.message}`;
        let stackTrace: string | undefined;

        // Format stack trace if available
        if (error.stack && Array.isArray(error.stack) && error.stack.length > 0) {
            const stackLines: string[] = [];
            for (const frame of error.stack) {
                const location =
                    frame.lineNumber !== undefined && frame.column !== undefined
                        ? `:${frame.lineNumber}:${frame.column}`
                        : frame.lineNumber !== undefined
                            ? `:${frame.lineNumber}`
                            : '';

                stackLines.push(`  at ${frame.methodName} (${frame.file}${location})`);
            }
            stackTrace = stackLines.join('\n');
        }

        return { errorMessage, stackTrace };
    } else {
        // Simple string or other value
        return { errorMessage: String(errorValueEntry.value ?? 'Unknown error') };
    }
}

/**
 * Formats timestamp to a readable date-time string
 */
function formatTimestamp(timestamp: string): string {
    const date = new Date(timestamp);
    return date
        .toISOString()
        .replace('T', ' ')
        .replace(/\.\d{3}Z$/, ' UTC');
}

/**
 * Formats duration in milliseconds to a human-readable string
 */
function formatDuration(ms: number): string {
    if (ms < 1000) {
        return `${ms}ms`;
    }
    const seconds = Math.floor(ms / 1000);
    if (seconds < 60) {
        return `${seconds}s`;
    }
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    return `${minutes}m ${remainingSeconds}s`;
}

/**
 * Type definitions for console logs structure
 */
interface StackFrame {
    file: string;
    methodName: string;
    arguments: unknown[];
    lineNumber?: number;
    column?: number;
    processed?: boolean;
}

interface ErrorObject {
    name: string;
    message: string;
    stack?: StackFrame[];
}

type LogValue = string | ErrorObject;

interface LogValueEntry {
    type: 'string' | 'Error';
    value: LogValue;
}

interface ConsoleLogEntry {
    type: string;
    severity: string;
    args: Array<{
        values: LogValueEntry[];
        thrown?: boolean;
        isArray: boolean;
    }>;
}

interface ConsoleLogsResponse {
    invocationLogs: ConsoleLogEntry[];
}

/**
 * Extracted error details from console logs
 */
interface ErrorDetails {
    errorMessage: string;
    stackTrace?: string;
}
TypeScriptSRConnectApi

import SRConnect from './api/srconnect';

/**
 * Retrieves invocation logs from ScriptRunner Connect REST API
 * @param params - Parameters including teamId, workspaceId, environmentName, startTime, and to
 * @returns Array of invocation logs matching the criteria
 */
export async function getInvocationLogs(params: GetInvocationLogsParams): Promise<InvocationLog[]> {
    const { teamId, workspaceId, environmentName, startTime, to, pageSize = 200, executionStatuses } = params;

    const allInvocations: InvocationLog[] = [];
    let nextToken: string | undefined;

    do {
        // Build query parameters for this request
        const currentQueryParams = new URLSearchParams({
            workspaces: workspaceId,
            environmentName: environmentName,
            environmentNameComparator: 'equals',
            from: startTime,
            to,
            pageSize: pageSize.toString(),
        });

        // Add execution statuses filter if provided
        if (executionStatuses && executionStatuses.length > 0) {
            // Join execution statuses as comma-separated values
            currentQueryParams.set('executionStatuses', executionStatuses.join(','));
        }

        // Add nextToken if available for pagination
        if (nextToken) {
            currentQueryParams.set('nextToken', nextToken);
        }

        // Make API call using Generic connector with retry logic for 429 responses
        const response = await fetchWithRetry(() =>
            SRConnect.fetch(`/v1/team/${teamId}/invocationLogs?${currentQueryParams.toString()}`),
        );

        if (!response.ok) {
            throw new Error(`Failed to fetch invocation logs: ${response.status} ${response.statusText}`);
        }

        const data = (await response.json()) as GetInvocationLogsResponse;

        // Add invocations from this page to the result array
        allInvocations.push(...data.invocations);

        // Check if there are more pages
        nextToken = data.nextToken;
    } while (nextToken);

    return allInvocations;
}

/**
 * Retrieves console logs for a specific invocation by downloading from the provided URL
 * @param params - Parameters including workspaceId and invocationId
 * @returns Parsed JSON content of the console logs, or null if download/parsing fails
 */
export async function getInvocationConsoleLogs(params: GetInvocationConsoleLogsParams): Promise<unknown | null> {
    const { workspaceId, invocationId } = params;

    try {
        // Get the download URL for console logs with retry logic for 429 responses
        const urlResponse = await fetchWithRetry(() =>
            SRConnect.fetch(`/v1/workspace/${workspaceId}/invocation/${invocationId}/consoleLogs`),
        );

        if (!urlResponse.ok) {
            return null;
        }

        const urlData = (await urlResponse.json()) as UrlDownloadableResource;

        // Download the file from the provided URL
        // Note: The download URL expires in 1 minute, so download immediately
        const downloadResponse = await fetch(urlData.url);

        if (!downloadResponse.ok) {
            return null;
        }

        // Parse the downloaded content as JSON
        return await downloadResponse.json();
    } catch {
        // Return null on any error (network, parsing, etc.)
        return null;
    }
}

/**
 * Helper function to retry fetch requests on 429 (Too Many Requests) responses
 * @param fetchFn - Function that returns a Promise<Response>
 * @param maxRetries - Maximum number of retry attempts (default: 5)
 * @param retryDelayMs - Delay in milliseconds before retrying (default: 1000)
 * @returns Response from successful request
 */
async function fetchWithRetry<T extends { ok: boolean; status: number; statusText: string }>(
    fetchFn: () => Promise<T>,
    maxRetries = 5,
    retryDelayMs = 1000,
): Promise<T> {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        const response = await fetchFn();

        // If successful or not a 429 error, return the response
        if (response.ok || response.status !== 429) {
            return response;
        }

        // If this is the last attempt, throw an error
        if (attempt === maxRetries) {
            throw new Error(`Failed after ${maxRetries + 1} attempts: ${response.status} ${response.statusText}`);
        }

        // Wait before retrying
        await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
    }

    // This should never be reached, but TypeScript requires it
    throw new Error('Unexpected error in fetchWithRetry');
}

/**
 * Response type for invocation logs from ScriptRunner Connect REST API
 */
interface GetInvocationLogsResponse {
    invocations: InvocationLog[];
    nextToken?: string;
}

/**
 * Individual invocation log entry
 */
export interface InvocationLog {
    invocationId: string;
    executionDuration: number;
    consoleLogsCount: number;
    httpLogsCount: number;
    workspace: {
        id: string;
        name: string;
    };
    environment: {
        id: string;
        name: string;
    };
    script: {
        id: string;
        name: string;
    };
    startTime: string;
    invocationType: string;
    triggerType: string;
    rootTriggerType: string;
    executionStatus: string;
    denialReason?: string;
}

/**
 * Parameters for getting invocation logs
 */
interface GetInvocationLogsParams {
    teamId: string;
    workspaceId: string;
    environmentName: string;
    startTime: string; // ISO 8601 format
    to: string; // ISO 8601 format - end time for filtering
    pageSize?: number; // Optional, defaults to 200, range 20-200
    executionStatuses?: string[]; // Optional, filter by execution statuses
}

/**
 * Response type for URL downloadable resource
 */
interface UrlDownloadableResource {
    url: string;
}

/**
 * Parameters for getting invocation console logs
 */
interface GetInvocationConsoleLogsParams {
    workspaceId: string;
    invocationId: string;
}

© 2025 ScriptRunner · Terms and Conditions · Privacy Policy · Legal Notice · Cookie Preferences