Skip to main content

Command Palette

Search for a command to run...

Agent SDK

GitLab agents

A GitLab agent wakes on merge request, note, and pipeline events from GitLab.com or a self-managed server, then writes its results back to the project. Declare onMergeRequest, onNote, or onPipeline on gitlabChannel() for each event that should wake it.

Connect a project

Name the project hook secret and list the projects that may wake the agent. Set apiBaseUrl for a self-managed server, and leave it out for GitLab.com.

// bot/channels/gitlab.tsimport { gitlabChannel } from "@grok/agent-sdk/channels/gitlab";export default gitlabChannel({  projects: ["acme/platform"],  webhookSecret: { secretName: "GITLAB_WEBHOOK_SECRET" },  apiBaseUrl: "https://gitlab.example.com/api/v4",});

Add a project webhook for https://<your-host>/<slug>/v1/channels/gitlab with the same secret. On Cursor hosting, a delivery with the wrong secret gets a 401 and never reaches your agent. ctx.gitlab calls the API with the token in GITLAB_TOKEN. To set both secrets on a deployment, see Add egress and secrets.

For GitLab.com on Cursor hosting, you can skip the project webhook. Set cursorAccount: { projects: ["acme/platform"] }, and events and ctx.gitlab calls go through your signed-in Cursor account.

Review a merge request

Read the latest diff, then post the finished review as a note:

// bot/channels/gitlab.tsimport {  createGitLabApi,  defaultGitLabAuth,  gitlabChannel,} from "@grok/agent-sdk/channels/gitlab";import { postMergeRequestNote } from "../lib/gitlab.js";export default gitlabChannel({  projects: ["acme/platform"],  onMergeRequest: async (ctx, mergeRequest) => {    if (mergeRequest.draft) {      return null;    }    const hasNewCommits =      mergeRequest.action === "update" && mergeRequest.oldrev !== undefined;    if (      mergeRequest.action !== "open" &&      mergeRequest.action !== "reopen" &&      !hasNewCommits    ) {      return null;    }    const api = createGitLabApi(      ctx.gitlab,      ctx.project.pathWithNamespace    );    const diffs = await api.listMergeRequestDiffs(mergeRequest.iid);    return {      // `auth` starts a model turn running as the event sender.      auth: defaultGitLabAuth(ctx),      message: `Review ${ctx.project.pathWithNamespace}!${mergeRequest.iid}.`,      context: [`<gitlab_diffs>${JSON.stringify(diffs)}</gitlab_diffs>`],    };  },  events: {    async "message.completed"(event, channel, ctx) {      if (ctx.session.purpose === "eval") {        return;      }      if (event.data.finishReason === "tool_call" || event.data.text === "") {        return;      }      const mergeRequestIid = channel.state.mergeRequestIid;      if (        (channel.state.eventType !== "merge_request" &&          channel.state.eventType !== "note") ||        mergeRequestIid === null      ) {        return;      }      await postMergeRequestNote(        channel.state.projectPath,        mergeRequestIid,        event.data.text      );    },  },});

postMergeRequestNote is a helper you write that posts through the GitLab API with GITLAB_TOKEN. It uses the project and merge request stored from the event.

Answer a note

Skip the agent's own notes, and answer a note that mentions the agent:

const AGENT_USERNAME = "cursor-reviewer";onNote: (ctx, note) => {  if (note.author?.username === AGENT_USERNAME) {    return null;  }  if (!note.body.includes(`@${AGENT_USERNAME}`)) {    return null;  }  return {    auth: defaultGitLabAuth(ctx),    message: note.body,  };},

Add this property to the same gitlabChannel() as the review handler.

Triage a failed pipeline

When a pipeline fails, the example marks the commit status as running and starts a triage turn. When the turn finishes, it sets the status to success or failure.

// bot/channels/gitlab.tsimport {  defaultGitLabAuth,  gitlabChannel,  gitlabChannelStateForEvent,} from "@grok/agent-sdk/channels/gitlab";import { setPipelineTriageStatus } from "../lib/gitlab.js";export default gitlabChannel({  projects: ["acme/platform"],  webhookSecret: { secretName: "GITLAB_WEBHOOK_SECRET" },  onPipeline: (ctx, pipeline) => {    if (pipeline.status !== "failed" || pipeline.sha === undefined) {      return null;    }    const sha = pipeline.sha;    return {      // `task` runs host code, and `send` starts the model turn.      task: async ({ send }) => {        await ctx.gitlab.setCommitStatus({          sha,          state: "running",          name: "cursor/pipeline-triage",          description: "Investigating failed pipeline",        });        await send(`Triage failed GitLab pipeline ${pipeline.id}.`, {          auth: defaultGitLabAuth(ctx),          state: {            ...gitlabChannelStateForEvent({              project: ctx.project,              headSha: sha,              eventType: ctx.eventType,            }),          },        });      },    };  },  events: {    async "message.completed"(event, channel, ctx) {      if (        ctx.session.purpose === "eval" ||        channel.state.eventType !== "pipeline" ||        channel.state.headSha === null ||        event.data.finishReason === "tool_call"      ) {        return;      }      await setPipelineTriageStatus(channel.state.projectPath, channel.state.headSha, "success");    },    async "turn.failed"(_event, channel, ctx) {      if (        ctx.session.purpose === "eval" ||        channel.state.eventType !== "pipeline" ||        channel.state.headSha === null      ) {        return;      }      await setPipelineTriageStatus(channel.state.projectPath, channel.state.headSha, "failed");    },  },});

setPipelineTriageStatus is a small helper you write in bot/lib/gitlab.ts. It updates the same cursor/pipeline-triage status through the GitLab API with GITLAB_TOKEN, using the project and commit from the session state, so the model never picks where it writes.

Each hook returns { auth }, { task }, or null, as handler results describes. Every event and note on one merge request resumes the same session.

Test with replay

Replay a merge request you can read against your local agent, or save the payloads as fixtures. gitlab replay lists every flag.

agent-sdk devagent-sdk gitlab replay acme/platform!42 --events pipeline --conclusion failedagent-sdk gitlab replay acme/platform!42 --events '*' --dry-run --fixtures-dir fixtures/gitlab