# Report failed Shipfox workflow runs (https://www.shipfox.io/docs/examples/report-failed-runs)

Description: Hear about every failed workflow run in Slack, with the failing step and what to do next.

Starts when: A Shipfox workflow run fails. Integrations: Slack.

## How it works

1. **A workflow run fails.** The workflow watches every workflow in the project.
2. **The workflow reads the failed run.** It gets the failed jobs and steps, the first error, and the last 15 log lines.
3. **You get a report on Slack.** The report suggests a next step. Optional: an agent adds a diagnosis in the thread.

## What it writes

- Slack: Posts a message for each failed run.

## Before you start

- Connect Slack and invite the Shipfox app to the report channel.

## Choices you make

When you set up this workflow, your coding agent asks you these questions. The workflow file on this page uses the default answers.

### Which projects should the report cover?

- **This project** (default): Reports runs of the project that holds this workflow. Requires the project ID.
- **Every project in the workspace**: Reports runs of every project into one channel. Add this workflow to one project only, or each failure is reported once per copy.

### Which workflows should the report cover?

- **Every workflow** (default): Reports failed runs of every synced workflow except this one.
- **Named workflow files**: Reports only the workflow files you list. A moved or renamed file stops being reported until you update the list.

### Should the report include cancelled runs?

- **Failed runs only** (default): Cancelled runs are not reported.
- **Failed and cancelled runs**: Also reports runs a person cancelled or that timed out. Runs replaced by a newer run in their concurrency group are never reported.

### Should an agent add a diagnosis from the failed step logs?

- **Report only** (default): No model or inference cost. The report lists the failed jobs and steps, a log excerpt, and a next step.
- **Add an agent diagnosis**: After each failed-run report, an agent reads the run's failed logs and replies in the report's thread. Each failed run uses inference.

## Models

When you set up this workflow, your coding agent suggests models that your workspace can use. You choose the model for each step.

- `diagnose`: Reads one failed run and its step logs, then writes a short diagnosis that cites the log lines. Tested with `gpt-6-luna` at low thinking.

## Set up this workflow

Open your coding agent in your repository and paste this prompt. The agent needs the [Shipfox MCP server](https://www.shipfox.io/docs/how-to/set-up-work/connect-mcp-client).

```text
Use Shipfox to create a workflow from the report-failed-runs template.
```

The workflow file, `.shipfox/workflows/report-failed-runs.yml`, with every default:

```yaml
# yaml-language-server: $schema=https://www.shipfox.io/docs/workflow.schema.json
# shipfox-template: report-failed-runs@1 notify=slack
name: Report failed Shipfox workflow runs
run_name: 'Report ${{ event.workflow.name }} #${{ event.run.number }}'
runner: shipfox

triggers:
  run_completed:
    source: shipfox
    event: run.completed
    # Every project in the workspace sends this event. Dev runs are never reported.
    filter: >-
      event.run.origin == "synced"
      && event.project.id == "replace-with-project-id"
      && (event.run.status == "failed"
      )

jobs:
  report:
    # A failed report would otherwise report itself.
    if: ${{ event.workflow.id != workflow.id }}
    checkout: false
    outputs:
      message_ts: ${{ steps.notify.outputs.ts }}
      channel: ${{ steps.notify.outputs.channel }}
    steps:
      - key: detail
        tool: get_workflow_run
        connection: shipfox
        with:
          run_id: ${{ event.run.id }}
        outputs:
          failed: '${{ result.jobs.filter(j, j.status == "failed").map(j, "`" + j.key + "`" + (j.selected_execution != null && j.selected_execution.steps.items.exists(s, s.status == "failed") ? " › " + j.selected_execution.steps.items.filter(s, s.status == "failed").map(s, "`" + s.name + "`").join(", ") : "") + (j.status_reason != null && j.status_reason != "step_failed" ? " (" + j.status_reason + ")" : "")).join(", ") }}'
          error: '${{ result.jobs.exists(j, j.status == "failed" && j.selected_execution != null && j.selected_execution.steps.items.exists(s, s.status == "failed" && s.error != null)) ? result.jobs.filter(j, j.status == "failed" && j.selected_execution != null && j.selected_execution.steps.items.exists(s, s.status == "failed" && s.error != null))[0].selected_execution.steps.items.filter(s, s.status == "failed" && s.error != null)[0].error.message : "" }}'
          next: >-
            ${{ result.run.status == "cancelled" ? "Check who or what cancelled the run if you did not expect it."
            : !result.run.has_started_job_execution ? "No job started. Open the run to see why, such as a missing runner, secret, or usage limit."
            : result.jobs.exists(j, j.status == "failed" && j.status_reason != null && j.status_reason in ["runner_lost", "provider_lost", "lease_expired"]) ? "The runner stopped responding. Rerun the failed jobs."
            : result.jobs.exists(j, j.status == "failed" && j.status_reason == "timed_out") ? "A job reached its time limit. Look for a stalled step before you rerun it."
            : result.jobs.exists(j, j.status == "failed" && j.status_reason != null && j.status_reason in ["condition_errored", "output_invalid", "output_too_large"]) ? "A workflow expression or output failed. Fix the workflow file, then rerun."
            : "Read the failed step's log, fix the cause, then rerun the failed jobs." }}

      # A cancelled run has no failed step, so it returns no log.
      - key: logs
        tool: get_step_logs
        connection: shipfox
        with:
          run_id: ${{ event.run.id }}
          failed_only: true
          tail_lines: 15
        outputs:
          excerpt: '${{ result.sections.size() == 0 ? "" : result.sections[0].content.size() > 800 ? "…" + result.sections[0].content.substring(result.sections[0].content.size() - 800, result.sections[0].content.size()) : result.sections[0].content }}'

      - key: notify
        tool: send_message
        connection: slack_notify
        with:
          # Replace with the ID of the channel that receives reports, such as C0ABC12345.
          channel_id: replace-with-slack-channel-id
          # Every part is capped so the message stays under Slack's 12,000-character limit.
          message: >-
            ${{ "**[" + (event.workflow.name.size() > 80 ? event.workflow.name.substring(0, 80) + "…" : event.workflow.name) + " #" + string(event.run.number) + "](https://app.shipfox.io/runs/" + event.run.id + ")** " + event.run.status
            + (event.run.attempt > 1 ? " on attempt " + string(event.run.attempt) : "")
            + " in " + (event.project.name.size() > 80 ? event.project.name.substring(0, 80) + "…" : event.project.name)
            + (steps.detail.outputs.failed != "" ? "\nFailed: " + (steps.detail.outputs.failed.size() > 300 ? steps.detail.outputs.failed.substring(0, 300) + "…" : steps.detail.outputs.failed) : "")
            + (steps.detail.outputs.error != "" ? "\nError: " + (steps.detail.outputs.error.size() > 300 ? steps.detail.outputs.error.substring(0, 300) + "…" : steps.detail.outputs.error) : "")
            + (steps.logs.outputs.excerpt != "" ? "\n```\n" + steps.logs.outputs.excerpt + "\n```" : "")
            + "\nNext: " + steps.detail.outputs.next }}
        outputs:
          ts: ${{ result.ts }}
          channel: ${{ result.channel }}
```

## Related examples

- [Investigate and repair default-branch CI failures](https://www.shipfox.io/docs/examples/fix-default-branch-ci)