Template Content
Not the template you're looking for? Browse more.
About the template
About ScriptRunner Connect
What is ScriptRunner Connect?
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.
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:
Slack Connector
Generic Connector (for ScriptRunner Connect REST API)
Slack API Connection
ScriptRunner Connect API Connection
CheckFailedNotifications Trigger
CheckFailedNotifications script0 0/15 * * * ? (runs every 15 minutes at the top of the hour)Configure the following Parameters in the ScriptRunner Connect web UI Parameters section:
| Parameter Name | Type | Description |
|---|---|---|
TEAM_ID | Text | Team ID where the workspace is located |
WORKSPACE_ID | Text | Workspace ID to filter logs for |
ENVIRONMENT_NAME | Text | Environment name to filter logs for |
SLACK_CHANNEL | Text | Slack channel where to send notifications |
CONCURRENCY | Number | How 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 UIWORKSPACE_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 typeSLACK_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 requirementsOnce configured, the integration runs automatically on the scheduled interval. No manual intervention is required.
TIMED_OUT: Script execution exceeded time limitFUNCTION_ERROR: Error occurred during script executionRUNTIME_ERROR: Runtime error during executionMALFORMED_PAYLOAD_ERROR: Invalid payload receivedManual Trigger:
CheckFailedNotifications script in the ScriptRunner Connect web UIVerification:
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:
Rate Limiting:
CONCURRENCY parameter if you experience rate limiting issuesTime Range Handling:
Memory and Performance:
CONCURRENCY if processing many failures simultaneouslyNotification Frequency:
Record Storage:
last-execution-timeNo Notifications Received:
SLACK_CHANNEL parameter is correctly configured (use channel name with # or channel ID)Missing Failures:
WORKSPACE_ID and ENVIRONMENT_NAME parameters match your workspace configurationlast-execution-time to see what time period is being checkedAPI Errors:
Performance Issues:
CONCURRENCY parameter if experiencing timeouts or rate limitsError Details Not Appearing:
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;
}
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