TroubleshootRestartFailedPlaybooks

Scans all non-closed alerts for playbook tasks in Error state and restarts them from the specific failed task. The script reopens each errored task and re-executes it, allowing the playbook to continue from where it stopped. Includes throttling support via the group_size and sleep_time arguments: after every group_size task restarts the script pauses for sleep_time seconds to avoid overloading the engine queue when scanning many alerts. This automation runs using the default Limited User role, unless you explicitly change the permissions. For more information, see the section about permissions here: https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.10/Cortex-XSOAR-Administrator-Guide/Automations

python · Troubleshoot

Source

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401


MAX_ALERTS_LIMIT = 200
DEFAULT_MAX_ALERTS = 200
DEFAULT_DAYS_BACK = 3
DEFAULT_GROUP_SIZE = 10
DEFAULT_SLEEP_TIME = 10


def get_alerts_with_errors(max_alerts: int, days_back: int) -> list[dict]:
    """
    Queries all non-closed alerts modified within the last `days_back` days
    and returns them.

    Args:
        max_alerts: Maximum number of alerts to retrieve (capped at MAX_ALERTS_LIMIT).
        days_back: Only include alerts modified within the last N days.

    Returns:
        A list of alert/incident dictionaries.
    """
    demisto.debug(f"Querying up to {max_alerts} non-closed alerts modified in the last {days_back} day(s).")

    from_date = (datetime.now(timezone.utc) - timedelta(days=days_back)).strftime("%Y-%m-%dT%H:%M:%SZ")

    result = demisto.executeCommand(
        "getIncidents",
        {
            "query": "-status:closed",
            "size": max_alerts,
            "fromdate": from_date,
        },
    )

    if is_error(result):
        raise DemistoException(f"Failed to query alerts: {get_error(result)}")

    incidents_data = result[0].get("Contents", {}).get("data")
    alerts = incidents_data if incidents_data else []
    demisto.debug(f"Found {len(alerts)} non-closed alerts modified in the last {days_back} day(s).")
    return alerts


def get_failed_tasks_for_alert(alert_id: str) -> list[dict]:
    """
    Retrieves all tasks in Error state for a given alert by fetching the full playbook
    via GET inv-playbook/{id} and filtering tasks client-side.

    Falls back to core-api-get if internalHttpRequest is not available.

    Args:
        alert_id: The alert/incident ID.

    Returns:
        A list of failed task dictionaries.
    """
    allowed_types = {"regular", "condition", "collection", "playbook"}
    uri = f"inv-playbook/{alert_id}"

    try:
        response = demisto.internalHttpRequest(
            method="GET",
            uri=uri,
        )

        if response and response.get("statusCode") == 200:
            playbook = json.loads(response.get("body", "{}"))
            return _extract_failed_tasks(playbook, allowed_types)

        demisto.debug(f"Internal HTTP request failed for alert {alert_id}: {response}")
        return []

    except (ValueError, Exception) as e:
        demisto.debug(f"Internal HTTP request not available for alert {alert_id}: {e}. Falling back to core-api-get.")
        return get_failed_tasks_via_api(alert_id, allowed_types)


def get_failed_tasks_via_api(alert_id: str, allowed_types: set[str]) -> list[dict]:
    """
    Retrieves failed tasks using core-api-get as a fallback.

    Args:
        alert_id: The alert/incident ID.
        allowed_types: Set of allowed task types to include.

    Returns:
        A list of failed task dictionaries.
    """
    uri = f"inv-playbook/{alert_id}"
    response = demisto.executeCommand(
        "core-api-get",
        {"uri": uri},
    )

    if is_error(response):
        demisto.debug(f"Failed to get playbook for alert {alert_id}: {get_error(response)}")
        return []

    playbook = response[0].get("Contents", {}).get("response", {})
    return _extract_failed_tasks(playbook, allowed_types)


def _extract_failed_tasks(playbook: dict, allowed_types: set[str]) -> list[dict]:
    """
    Extracts tasks in Error state from a playbook object.

    Each returned task is stamped with `_playbookName` (the top-level playbook
    name from the inv-playbook payload) so that callers can report a meaningful
    PlaybookName even when the task has no `ancestors` (i.e. it lives directly
    in the alert's main playbook rather than inside a nested sub-playbook).

    Args:
        playbook: The full playbook dictionary returned by inv-playbook/{id}.
        allowed_types: Set of task types to include (e.g., {"regular", "condition", "collection"}).

    Returns:
        A filtered list of failed task dictionaries.
    """
    if not playbook or not isinstance(playbook, dict):
        return []

    tasks = playbook.get("tasks", {})
    if not isinstance(tasks, dict):
        return []

    top_level_playbook_name = playbook.get("name") or playbook.get("playbookName") or ""

    failed_tasks = []
    for task_data in tasks.values():
        if task_data.get("state") == "Error" and task_data.get("type") in allowed_types:
            # Stamp the top-level playbook name so the caller can fall back to it
            # when `ancestors` is empty.
            task_data["_playbookName"] = top_level_playbook_name
            failed_tasks.append(task_data)

    return filter_playbook_failures(failed_tasks) if failed_tasks else []


def filter_playbook_failures(response: list) -> list:
    """
    Filters out tasks of type 'playbook' from the response if their name appears
    in the ancestors of any other task in the list. This prevents duplicate errors
    when the real failure is in an internal task of the playbook.

    Args:
        response: List of failure tasks.

    Returns:
        The filtered list of tasks.
    """
    ancestors = set()
    for task in response:
        ancestors.update(task.get("ancestors", []))

    filtered_response = [
        task for task in response if not (task.get("type") == "playbook" and task.get("task", {}).get("name") in ancestors)
    ]
    return filtered_response


def restart_task(task_id: str, incident_id: str) -> dict:
    """
    Reopens a failed task and re-executes it.

    Args:
        task_id: The task ID to restart.
        incident_id: The incident/alert ID containing the task.

    Returns:
        A dict with the result: {"success": True/False, "error": "..."}.
    """
    # Step 1: Reopen the task
    reopen_result = demisto.executeCommand("taskReopen", {"id": task_id, "incidentId": incident_id})
    if is_error(reopen_result):
        error_msg = get_error(reopen_result)
        demisto.debug(f"Failed to reopen task {task_id} on alert {incident_id}: {error_msg}")
        return {"success": False, "error": f"Failed to reopen: {error_msg}"}

    # Step 2: Re-execute the task
    body: dict = {"invId": incident_id, "inTaskID": task_id}

    if is_demisto_version_ge("6.2"):
        body = {"taskinfo": body}

    try:
        response = demisto.internalHttpRequest(
            method="POST",
            uri="inv-playbook/task/execute",
            body=json.dumps(body),
        )

        if response and response.get("statusCode") == 200:
            demisto.debug(f"Successfully restarted task {task_id} on alert {incident_id}.")
            return {"success": True, "error": ""}

        error_msg = f"Status {response.get('statusCode')}: {response.get('body', '')}"
        demisto.debug(f"Failed to execute task {task_id} on alert {incident_id}: {error_msg}")
        return {"success": False, "error": f"Failed to execute: {error_msg}"}

    except Exception as e:
        demisto.debug(f"internalHttpRequest failed for task {task_id} on alert {incident_id}: {e}")
        return {"success": False, "error": f"Failed to execute: {e}"}


def restart_all_failed_tasks(
    alerts: list[dict],
    group_size: int,
    sleep_time: int,
) -> tuple[list[dict], list[dict]]:
    """
    Iterates over all alerts, finds failed tasks, and restarts them with throttling.

    Args:
        alerts: List of alert/incident dictionaries.
        group_size: Number of tasks to restart before sleeping.
        sleep_time: Seconds to sleep between groups.

    Returns:
        A tuple of (restarted_tasks, failed_to_restart) lists.
    """
    restarted_tasks = []
    failed_to_restart = []
    total_restarted = 0

    for alert in alerts:
        alert_id = str(alert.get("id", ""))
        if not alert_id:
            continue

        failed_tasks = get_failed_tasks_for_alert(alert_id)
        if not failed_tasks:
            continue

        demisto.debug(f"Found {len(failed_tasks)} failed tasks for alert {alert_id}.")

        for task in failed_tasks:
            task_id = task.get("id", "")
            task_name = task.get("task", {}).get("name", "Unknown")
            # Prefer the deepest ancestor (the innermost sub-playbook actually
            # containing the task). Fall back to the top-level playbook name
            # stamped by _extract_failed_tasks for tasks that have no ancestors
            # (i.e. they live directly in the alert's main playbook).
            ancestors = task.get("ancestors") or []
            playbook_name = ancestors[-1] if ancestors else task.get("_playbookName", "")

            if not task_id:
                continue

            demisto.info(f"Restarting task '{task_name}' (ID: {task_id}) on alert {alert_id}")
            result = restart_task(task_id, alert_id)

            if result["success"]:
                restarted_tasks.append(
                    {
                        "IncidentID": alert_id,
                        "TaskID": task_id,
                        "TaskName": task_name,
                        "PlaybookName": playbook_name,
                    }
                )
            else:
                failed_to_restart.append(
                    {
                        "IncidentID": alert_id,
                        "TaskID": task_id,
                        "TaskName": task_name,
                        "PlaybookName": playbook_name,
                        "Error": result["error"],
                    }
                )

            total_restarted += 1

            # Throttle: pause for `sleep_time` seconds after every `group_size` task restarts
            # to avoid overloading the engine queue when scanning many alerts.
            if total_restarted % group_size == 0:
                demisto.debug(f"Reached group size {group_size}, sleeping for {sleep_time} seconds.")
                time.sleep(sleep_time)  # pylint: disable=E9003

    return restarted_tasks, failed_to_restart


def main() -> None:
    try:
        args = demisto.args()
        requested_max_alerts = int(args.get("max_alerts", DEFAULT_MAX_ALERTS))
        # Enforce the hard cap: if the user requests more than MAX_ALERTS_LIMIT,
        # silently clamp to MAX_ALERTS_LIMIT. Anything <= the limit is honored.
        max_alerts = min(requested_max_alerts, MAX_ALERTS_LIMIT)
        if requested_max_alerts > MAX_ALERTS_LIMIT:
            demisto.debug(
                f"Requested max_alerts={requested_max_alerts} exceeds the hard cap; " f"clamping to {MAX_ALERTS_LIMIT}."
            )

        days_back = int(args.get("days_back", DEFAULT_DAYS_BACK))
        if days_back < 1:
            raise DemistoException("The days_back argument must be 1 or higher.")

        group_size = int(args.get("group_size", DEFAULT_GROUP_SIZE))
        sleep_time = int(args.get("sleep_time", DEFAULT_SLEEP_TIME))

        if group_size < 1:
            raise DemistoException("The group_size argument must be 1 or higher.")

        # Step 1: Get all non-closed alerts modified within the last `days_back` days.
        alerts = get_alerts_with_errors(max_alerts, days_back)
        if not alerts:
            return return_results(f"No non-closed alerts were found in the last {days_back} day(s).")

        # Step 2: Restart all failed tasks across all alerts
        restarted_tasks, failed_to_restart = restart_all_failed_tasks(alerts, group_size, sleep_time)

        if not restarted_tasks and not failed_to_restart:
            return return_results("No failed tasks were found across the queried alerts.")

        # Step 3: Build output
        readable_parts = []

        if restarted_tasks:
            readable_parts.append(
                tableToMarkdown(
                    f"Successfully Restarted Tasks ({len(restarted_tasks)})",
                    restarted_tasks,
                    headers=["IncidentID", "PlaybookName", "TaskName", "TaskID"],
                    headerTransform=pascalToSpace,
                )
            )

        if failed_to_restart:
            readable_parts.append(
                tableToMarkdown(
                    f"Failed to Restart Tasks ({len(failed_to_restart)})",
                    failed_to_restart,
                    headers=["IncidentID", "PlaybookName", "TaskName", "TaskID", "Error"],
                    headerTransform=pascalToSpace,
                )
            )

        readable_output = "\n".join(readable_parts)

        outputs = {
            "TotalRestarted": len(restarted_tasks),
            "TotalFailed": len(failed_to_restart),
            "TotalAlerts": len(alerts),
            "RestartedTask": restarted_tasks,
            "FailedToRestart": failed_to_restart,
        }

        return_results(
            CommandResults(
                outputs_prefix="TroubleshootRestartFailedPlaybooks",
                outputs=outputs,
                readable_output=readable_output,
            )
        )

    except Exception as e:
        return_error(f"Failed to restart failed playbooks: {e}")


if __name__ in ["builtins", "__main__"]:
    main()

README

Scans all non-closed alerts for playbook tasks in Error state and restarts them from the specific failed task.
The script reopens each errored task and re-executes it, allowing the playbook to continue from where it stopped.
Includes throttling support via the group_size and sleep_time arguments: after every group_size task restarts the script pauses for sleep_time seconds to avoid overloading the engine queue when scanning many alerts.

This automation runs using the default Limited User role, unless you explicitly change the permissions.
For more information, see the section about permissions here:
https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.10/Cortex-XSOAR-Administrator-Guide/Automations

Script Data


Name Description
Script Type python3
Tags DemistoAPI, troubleshoot
Cortex XSOAR Version 5.0.0

Inputs


Argument Name Description
max_alerts Maximum number of non-closed alerts to scan for failed tasks. The maximum allowed value is 200; any larger value will be clamped to 200.
days_back Only scan non-closed alerts that were modified within this number of days back. Defaults to 3.
group_size Number of tasks to restart before pausing. Helps avoid overwhelming the system.
sleep_time Number of seconds to pause between each group of restarted tasks.

Outputs


Path Description Type
TroubleshootRestartFailedPlaybooks.TotalRestarted Total number of tasks that were successfully restarted. Number
TroubleshootRestartFailedPlaybooks.TotalFailed Total number of tasks that failed to restart. Number
TroubleshootRestartFailedPlaybooks.TotalAlerts Total number of alerts that were scanned. Number
TroubleshootRestartFailedPlaybooks.RestartedTask.IncidentID The alert ID where the task was restarted. String
TroubleshootRestartFailedPlaybooks.RestartedTask.TaskID The ID of the restarted task. String
TroubleshootRestartFailedPlaybooks.RestartedTask.TaskName The name of the restarted task. String
TroubleshootRestartFailedPlaybooks.RestartedTask.PlaybookName The playbook name containing the restarted task. String
TroubleshootRestartFailedPlaybooks.FailedToRestart.IncidentID The alert ID where the task failed to restart. String
TroubleshootRestartFailedPlaybooks.FailedToRestart.TaskID The ID of the task that failed to restart. String
TroubleshootRestartFailedPlaybooks.FailedToRestart.TaskName The name of the task that failed to restart. String
TroubleshootRestartFailedPlaybooks.FailedToRestart.PlaybookName The playbook name containing the task that failed to restart. String
TroubleshootRestartFailedPlaybooks.FailedToRestart.Error The error message explaining why the task failed to restart. String