---
title: "Fix failing CI"
description: "Build a durable chat backend that explains why a GitHub Actions run failed and suggests a fix, with Vercel Workflow and the ci-ops preset."
canonical_url: "https://github-tools.com/guides/fix-failing-ci"
---
# Fix failing CI

> Build a durable chat backend that explains why a GitHub Actions run failed and suggests a fix, with Vercel Workflow and the ci-ops preset.

To find out why a GitHub Actions run failed with an AI agent, give a model the `ci-ops` preset and ask it about the run: `getCiFailureContext` returns the failing checks, jobs, steps and the end of each failed job's log in one call, and the model explains the likely cause before suggesting a fix. This guide runs that agent in [Vercel Workflow](https://github-tools.com/integrations/workflow), so a long investigation survives timeouts and redeploys.

## What you'll build

A chat backend you ask "why did CI fail on #128?". The agent finds the failing run, reads the failed jobs and the end of their logs, and explains the cause in plain language. Every model call and tool call runs as a workflow step, so an investigation resumes where it stopped after a timeout or a redeploy.

Rerunning, cancelling or triggering a workflow asks for [approval](https://github-tools.com/approval), which pauses the run until someone answers.

<ci-failure-flow>



</ci-failure-flow>

## Before you start

- A server app with a chat UI, such as Nuxt or Next.js. The routes below use Nitro handlers from Nuxt; adapt them to your framework.
- A GitHub connector named `github/my-connector` in [Vercel Connect](https://github-tools.com/auth#create-a-github-connector), installed on the repositories the bot watches, with its token pulled locally.

## 1. Install the packages

<code-group>

```bash [pnpm]
pnpm add @github-tools/sdk @vercel/connect workflow @ai-sdk/workflow ai zod
```

```bash [npm]
npm install @github-tools/sdk @vercel/connect workflow @ai-sdk/workflow ai zod
```

```bash [yarn]
yarn add @github-tools/sdk @vercel/connect workflow @ai-sdk/workflow ai zod
```

```bash [bun]
bun add @github-tools/sdk @vercel/connect workflow @ai-sdk/workflow ai zod
```

</code-group>

The bot is three files:

```text
server/
├── workflows/
│   └── ci-failure-bot.ts                  # the agent
└── api/
    ├── ci-chat.post.ts                    # starts a run
    └── ci-chat/[runId]/stream.get.ts      # reconnects to a run
```

## 2. Write the workflow

The workflow holds the whole agent:

```ts [server/workflows/ci-failure-bot.ts]
import { createDurableGithubAgent } from '@github-tools/sdk/workflow'
import { connectGithubToken } from '@github-tools/sdk/connect'
import type { ModelCallStreamPart } from '@ai-sdk/workflow'
import type { ModelMessage } from 'ai'
import { getWritable } from 'workflow'

export async function ciFailureBot(messages: ModelMessage[]) {
  'use workflow'

  const agent = createDurableGithubAgent({
    model: 'anthropic/claude-opus-5.5',
    token: connectGithubToken('github/my-connector', { preset: 'ci-ops' }),
    preset: 'ci-ops',
    additionalInstructions: 'Explain the likely cause of a failure in plain language before suggesting a fix. Do not rerun anything unless asked.',
  })

  await agent.stream({ messages, writable: getWritable<ModelCallStreamPart>() })
}
```

The `ci-ops` [preset](https://github-tools.com/presets) has 21 tools. `getCiFailureContext` answers most questions in one call, and `getWorkflowJobLogs` reads more of a job's log when the error is higher up. Its four writes, `triggerWorkflow`, `cancelWorkflowRun`, `rerunWorkflowRun` and `createCommitStatus`, ask for approval, and this agent only reads run status, so it asks only when you tell it to act. Pass the same `preset` to `connectGithubToken`, so the token has the permissions those tools need and no more. That includes Commit statuses (write) for `createCommitStatus`: add it to the connector's GitHub App, or [leave the tool out of the token](https://github-tools.com/auth#leave-a-tool-out-of-the-token).

## 3. Start a run from a route

The route starts a run and streams it back as UI message chunks:

```ts [server/api/ci-chat.post.ts]
import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'
import { convertToModelMessages, createUIMessageStreamResponse, type UIMessage } from 'ai'
import { start } from 'workflow/api'
import { ciFailureBot } from '../workflows/ci-failure-bot'

export default defineEventHandler(async (event) => {
  // Anyone who can start a run reads CI data with the connector's access.
  await requireUserSession(event)
  const { messages } = await readBody<{ messages: UIMessage[] }>(event)
  const run = await start(ciFailureBot, [await convertToModelMessages(messages)])

  return createUIMessageStreamResponse({
    stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
    headers: { 'x-workflow-run-id': run.runId },
  })
})
```

`requireUserSession` comes from [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils); use your app's own check elsewhere. Without it, anyone who finds the route can read your CI logs through the connector.

The reconnect route reads `getRun(runId).getReadable({ startIndex })` through the same transform, and the client talks to both routes with `WorkflowChatTransport`. [Vercel Workflow](https://github-tools.com/integrations/workflow#reconnect-after-a-reload) has both.

## Run it

Start your app's dev server, open the chat, and ask about a failing run, such as "why did the last run on main fail in vercel-labs/github-tools?". The answer streams in while the agent reads the run. `npx workflow web` shows each run and the steps it took.

## FAQ

<accordion>
<accordion-item label="Can the diagnosis start before anyone asks?">

Yes. Start the same workflow from a GitHub webhook on `workflow_run` events with `conclusion: failure`, and post the result where your team reads it.

</accordion-item>

<accordion-item label="Can the agent rerun the failed jobs?">

Yes, with your approval. `rerunWorkflowRun` is in the `ci-ops` preset and asks before it runs. The run pauses until someone answers, even hours later.

</accordion-item>

<accordion-item label="Why not requireApproval: 'auto'?">

`'auto'` isn't available in a durable workflow. Set each write to `true` or `false` instead. See [Vercel Workflow](https://github-tools.com/integrations/workflow#approval).

</accordion-item>

<accordion-item label="How much of the log does the model read?">

`getWorkflowJobLogs` returns the last 200 lines of a job without timestamps by default, and the model can raise `maxLines` up to 2,000 when the error is higher up.

</accordion-item>
</accordion>


## Sitemap

See the full [sitemap](https://github-tools.com/sitemap.md) for all pages.
