AI agents
Three Claude-powered jobs that run the prompts a group gives them: claude:work on any branch, automatically on a new one such as a branch created from an issue, claude:review on a merge request when you play it, or on every push once a group or project opts in, and claude:scheduled on any schedule. The jobs ship no prompts: a group keeps them as Claude Code commands in its own agent image, or sets them as text; see Prompts.
Import
pipelines/default.yaml includes the agent jobs, so a project that includes the default pipeline already has them. They stay off until the project or its group has a Claude credential: every agent job requires CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in its rules, so without one the jobs don't appear in the pipeline at all. Setting the token on a group turns the agents on for all of its projects; see Tokens.
A project that doesn't use the default pipeline includes the file itself:
include:
- project: hosstio/gitlab-pipelines
file: pipelines/agents.yamlThis file defines no workflow: block on purpose. Include it alongside another pipeline file that supplies one, because the last included workflow: wins and a file that set its own would override whatever it is combined with. The jobs gate themselves with strict rules instead.
All four jobs run in the agents stage, which helpers/default.yaml puts before extra. claude:review waits for the earlier stages and runs even when they fail, so it sees their results. The other three set needs: [], so they start with the pipeline, and a failed earlier stage never skips a play button. A project that defines its own stages: list has to add agents to it.
Jobs
| Job | When |
|---|---|
claude:work | Branch and merge request pipelines, except on the default branch and on schedules; automatic on a branch without changes, such as one created from an issue, manual everywhere else |
claude:review | Merge request pipelines, manual; automatic after the other stages on every push when AGENT_REVIEW_AUTOMATIC is "true" |
claude:scheduled | Scheduled pipelines whose schedule sets AGENT_SCHEDULED_PROMPT |
Each job is named <agent>:<task>, so a Codex sibling of the review would be codex:review.
How the agents work
The jobs run Claude Code on the image in AGENT_CLAUDE_IMAGE, the agents:claude image by default, which also has glab and Git, and give it a GitLab token. Claude reads what it needs itself: the merge request, its linked issue, the discussions, the pipeline's jobs and their logs, and other issues or merge requests that those mention. It writes to GitLab itself too, with glab and git push. The prompts say what to read and what to write.
The jobs keep the parts that must not depend on the model: when each job runs, the budget and turn caps, the checkout of the branch tip, the git identity, and posting the reports to Slack. Every commit Claude makes is in the name of the user who started the pipeline or played the job, and a commit-msg hook adds an Assisted-by: Claude Code (<job>) trailer. Every comment, reply, issue and merge request description Claude posts ends with a line naming and linking the job, such as "Posted by claude:review", so readers can tell them apart from a person's even when the token is a person's.
Repositories and skills
The review, work and scheduled jobs check out the git repositories in GIT_REPOSITORIES for Claude to read, such as documentation or a specification the work has to agree with, and give it the skills of the repositories in AGENT_SKILLS. The startup scripts of the agents image do this after the job's token check: the checkouts go to /repositories/<group>/<project>, and every directory with a SKILL.md becomes a skill in ~/.claude/skills. They clone as AGENT_GITLAB_AUTH_TOKEN, so that token needs read access to every private repository on the lists. The checkouts are references: they can be open source projects you don't own, and the prompts never change or file anything in them.
Both lists are empty in these templates. A group sets them as CI/CD variables, or in the variables of its own pipelines repository:
glab variable set GIT_REPOSITORIES --group <group> \
--value "https://gitlab.com/<group>/docs.git https://github.com/<owner>/<project>.git"
glab variable set AGENT_SKILLS --group <group> --value "https://github.com/anthropics/skills.git"A group variable overrides any variable a job sets in YAML, so each job reads its own pair, which defaults to the shared lists: AGENT_REVIEW_REPOSITORIES and AGENT_REVIEW_SKILLS, AGENT_WORK_REPOSITORIES and AGENT_WORK_SKILLS, AGENT_SCHEDULED_REPOSITORIES and AGENT_SCHEDULED_SKILLS. To give claude:work more context than the review, set AGENT_WORK_REPOSITORIES on the group to $GIT_REPOSITORIES plus the extra URLs. A schedule's variables override the group's, so a schedule sets AGENT_SCHEDULED_REPOSITORIES for its own run.
Models and effort
Each job sets AGENT_CLAUDE_MODEL and AGENT_CLAUDE_EFFORT for its kind of work:
| Job | Model | Effort | Why |
|---|---|---|---|
claude:review | opus | high | Finding subtle bugs is what a review is for |
claude:work | opus | high | Multi-step coding with tests and git, and conflict resolutions, where merging two changes wrongly still builds |
claude:scheduled | sonnet | high | Long, read-heavy tasks such as updates; a schedule sets AGENT_CLAUDE_MODEL and AGENT_CLAUDE_EFFORT for other work, such as haiku for a status update |
The aliases resolve in the Claude Code CLI, which the agents:claude image pins, so a job keeps its model until the image moves to a newer Claude Code. Override either variable on a job to change it, for example AGENT_CLAUDE_EFFORT: "xhigh" on claude:work for harder issues.
How the work job works
claude:work shows in every branch and merge request pipeline, except on the default branch and on schedules, once a group or project sets AGENT_PROMPT_WORK. The job only checks out the branch's latest commit, so it's fine to play it in an older pipeline, and hands Claude that prompt. Its timeout is 60 minutes, which leaves room for a prompt that waits for the pipeline of its own push.
Starting from an issue
GitLab's Create merge request button on an issue creates a branch named after the issue, such as 7-update-inventree, and a merge request without commits. GitLab gives a merge request without commits no merge request pipeline, so the only pipeline is the branch pipeline it starts when it creates the branch. That branch has no changes against the default branch, so claude:work runs there automatically. A work prompt that reads the issue from the branch name can implement it and push, which starts the merge request pipeline. The branch pipeline still runs the other jobs, on the default branch's code.
From the command line, push a new branch that starts with the issue number, and claude:work starts the same way:
git switch --create 42-add-a-status-page origin/main
git push --set-upstream origin 42-add-a-status-page
glab mr create --related-issue 42 --description "Closes #42" --draft --yesPlayed runs
On a branch with changes, the job is a manual play button: play it after a review or a failed job, and the prompt decides what Claude does with the unresolved threads, the failed jobs and a conflict with the target branch. Conflicts usually appear when the target branch moves, which starts no pipeline on the merge request, so play claude:work from the latest pipeline. The job isn't interruptible: its own pushes start new pipelines, and with the project's Auto-cancel redundant pipelines setting on, those would cancel an interruptible job before it finishes.
Docker
claude:work and claude:scheduled extend .docker from the Docker helper, so they run a docker:dind service and logs in to the project's container registry. Claude can then run a tool the image lacks in a container, for example docker run --rm -v "$CI_PROJECT_DIR:$CI_PROJECT_DIR" -w "$CI_PROJECT_DIR" alpine/helm lint charts/example, or pull the project's own images. GitLab shares the /builds directory between a job and its services, so the checkout mounts into such a container. Claude can also start the project's docker compose services, such as a database; their published ports are reachable at host docker, not localhost. The service needs a runner that allows privileged containers, as GitLab's hosted runners do. On a runner without privileged mode, turn Docker off for the project in its .gitlab-ci.yml:
claude:work:
services: []
before_script:
- !reference [.claude, before_script]
claude:scheduled:
services: []
before_script:
- !reference [.claude, before_script]Rules
The play button shows on every branch with changes, because GitLab rules cannot see threads or failed jobs. A merge request pipeline compares changes with the merge request's target branch, and a branch pipeline with the default branch (rules:changes:compare_to), so a branch without changes matches the automatic rule instead.
How the review works
claude:review is a manual job in every merge request pipeline once a group or project sets AGENT_PROMPT_REVIEW, so a review costs nothing until someone plays it. Play it from the MR's latest pipeline once the other stages are done. To review every push automatically, set AGENT_REVIEW_AUTOMATIC to "true" as a CI/CD variable on the group or project: the job then runs after the other stages, also when they fail. You can retry it from its job page either way. The job skips the review when the MR has no changes, or when a newer push exists, which its own pipeline reviews.
The job runs after the other stages without needs, so GitLab unpacks their artifacts in the checkout, such as test reports or the Helm manifest diff, for the prompt to point Claude at. It checks out the merge request's latest commit and allows read-only git, glab and printenv. AGENT_REVIEW_MARKER is an HTML comment a review prompt can tag its summary note with, so a later review edits that note instead of adding one. GitLab sends no notification when a note is edited, but each new thread notifies as usual.
Reviews are advisory. The job runs with allow_failure: true, like the other merge request jobs, so a failed or over-budget review never blocks the merge request. The scheduled jobs are not allowed to fail: nobody watches a nightly run, so a failure turns the pipeline red and GitLab sends its failed-pipeline email. With the project's Auto-cancel redundant pipelines setting on, a newer push cancels a review that is still running for an older one.
Tokens
Claude authenticates one of two ways:
| Variable | Where it comes from |
|---|---|
CLAUDE_CODE_OAUTH_TOKEN | claude setup-token (subscription auth). Set it as a masked group-level CI/CD variable; these tokens last one year |
ANTHROPIC_API_KEY | An Anthropic API key. Takes precedence when both are set |
For GitLab, the jobs hand AGENT_GITLAB_AUTH_TOKEN to glab and to git push. Without it they fall back to GITLAB_AUTH_TOKEN, which defaults to CI_JOB_TOKEN; that token cannot read MR discussions, post notes or threads, create issues, or push, so with it the agent jobs skip with a warning. Give the agents their own token rather than a personal one: their threads and pushes then show as a bot instead of a colleague, the token carries only the rights the agents need, and revoking it leaves the other jobs alone. Set AGENT_GITLAB_AUTH_TOKEN as a masked, hidden CI/CD variable to one of these:
- A project access token with the Developer role and
apiandwrite_repositoryscope, set on the project. Claude can then read and write in that project only. This is the setup GitLab's own Claude Code integration uses. - A group access token with the same role and scopes, set once on the group. Claude can then also read related projects in the group, such as an issue or merge request in a sibling repository. It can also write to every one of them.
- On GitLab.com's Free plan, where project and group access tokens are not available: a personal access token with the same scopes from a separate bot user, added to the group with the Developer role.
When AGENT_GITLAB_AUTH_TOKEN is unset and GITLAB_AUTH_TOKEN is a person's token, the agents act as that person, with all of their rights.
With the Developer role, Claude can push to merge request branches and open issues and merge requests, but cannot push to or merge into protected branches.
Claude reads text that anyone with access to the project can write: issue descriptions, comments, commit messages. Such text can contain instructions, and Claude acts with the token's rights, so the token's scope limits what such instructions can make it do.
SLACK_AUTHENTICATION_TOKEN lets claude:scheduled post its final answer; see the Slack helper.
Variable names
Every variable these jobs define starts with AGENT_: AGENT_ alone for one that every agent shares, such as AGENT_GITLAB_AUTH_TOKEN, AGENT_PROMPT_ for the prompt of a job, such as AGENT_PROMPT_REVIEW, and AGENT_CLAUDE_ for a Claude setting, such as AGENT_CLAUDE_MODEL. A Codex or OpenCode sibling would use AGENT_CODEX_ and AGENT_OPENCODE_. Variables a CLI reads itself keep the CLI's own names, so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY are the Claude CLI's, not the pipelines'.
Variables
Claude
| Variable | Default | Description |
|---|---|---|
AGENT_CLAUDE_IMAGE | registry.gitlab.com/hosstio/docker-images/agents/main:claude | The image the agent jobs run; see Agent image |
AGENT_CLAUDE_MODEL | "" | Model to use, as an alias or a full model name; each job sets its own, see Models and effort |
AGENT_CLAUDE_EFFORT | "" | Effort level; each job sets its own |
AGENT_CLAUDE_MAX_BUDGET_USD | 1.00 | Dollar ceiling per invocation |
AGENT_CLAUDE_MAX_TURNS | 30 | Turn ceiling per invocation |
AGENT_CLAUDE_PERMISSION_MODE | dontAsk | Denies anything outside AGENT_CLAUDE_ALLOWED_TOOLS and the read-only set |
AGENT_CLAUDE_ALLOWED_TOOLS | Read,Grep,Glob,Bash(git diff *),Bash(git log *) | Tools the model may use |
AGENT_CLAUDE_OUTPUT_FILE | /tmp/claude-output.json | Where the JSON run output is written |
AGENT_ASSISTED_BY | Claude Code | Tool name in the Assisted-by trailer of agent commits |
AGENT_GITLAB_AUTH_TOKEN | $GITLAB_AUTH_TOKEN | GitLab token the agents read and write with; see Tokens |
Repositories and skills
| Variable | Default | Description |
|---|---|---|
GIT_REPOSITORIES | "" | Git URLs, separated by spaces, of the repositories the agents read; see Repositories and skills |
AGENT_SKILLS | "" | Git URLs of skill repositories |
GIT_REPOSITORIES_DIR | /repositories | Where the checkouts go |
AGENT_REVIEW_REPOSITORIES, AGENT_WORK_REPOSITORIES, AGENT_SCHEDULED_REPOSITORIES | $GIT_REPOSITORIES | The repositories of one job |
AGENT_REVIEW_SKILLS, AGENT_WORK_SKILLS, AGENT_SCHEDULED_SKILLS | $AGENT_SKILLS | The skill repositories of one job |
Work
| Variable | Default | Description |
|---|---|---|
AGENT_PROMPT_WORK | The prompt; the job runs only when it's set, see Prompts | |
AGENT_CLAUDE_PERMISSION_MODE | acceptEdits | Per-job override so Claude may write files |
AGENT_CLAUDE_ALLOWED_TOOLS | Read,Grep,Glob,Edit,Write,Bash | Per-job override: all Bash commands, for git, glab, docker and quick checks |
AGENT_CLAUDE_MAX_BUDGET_USD | 5.00 | Per-job budget override |
AGENT_CLAUDE_MAX_TURNS | 100 | Per-job turn override |
Review
| Variable | Default | Description |
|---|---|---|
AGENT_PROMPT_REVIEW | The prompt; the job runs only when it's set, see Prompts | |
AGENT_REVIEW_AUTOMATIC | "true" reviews every push automatically; unset, the review is a manual job | |
AGENT_REVIEW_MARKER | <!-- gitlab-pipelines:review --> | HTML comment a review prompt can tag its summary note with, so a later review edits that note |
AGENT_CLAUDE_ALLOWED_TOOLS | Read,Grep,Glob,Bash(git diff *),Bash(git log *),Bash(git show *),Bash(glab *),Bash(printenv *) | Per-job override: read-only git, glab, and printenv for the CI variables a prompt names |
AGENT_CLAUDE_MAX_BUDGET_USD | 2.00 | Per-job budget override |
AGENT_CLAUDE_MAX_TURNS | 60 | Per-job turn override |
Scheduled
| Variable | Default | Description |
|---|---|---|
AGENT_SCHEDULED_PROMPT | The task, set on the schedule as text or a command of the agent image, such as /update; see Prompts | |
AGENT_SCHEDULED_AGENT | The agent that runs the schedule; unset or claude runs claude:scheduled | |
AGENT_CLAUDE_MODEL | sonnet | Per-job override; a schedule sets its own for its run |
AGENT_CLAUDE_EFFORT | high | Per-job override |
AGENT_CLAUDE_PERMISSION_MODE | acceptEdits | Per-job override so Claude may write files |
AGENT_CLAUDE_ALLOWED_TOOLS | Read,Grep,Glob,Edit,Write,Bash,WebFetch | Per-job override: Bash for git, glab, docker and curl, WebFetch for release notes and docs |
AGENT_CLAUDE_MAX_BUDGET_USD | 5.00 | Per-job budget override |
AGENT_CLAUDE_MAX_TURNS | 150 | Per-job turn override |
Prompts
The jobs bring no prompt of their own. claude:review runs AGENT_PROMPT_REVIEW, claude:work runs AGENT_PROMPT_WORK, and claude:scheduled runs the schedule's AGENT_SCHEDULED_PROMPT; a job without its prompt doesn't appear in the pipeline. A prompt is text, or a Claude Code command that the image carries, such as /review. Text after a command's name narrows one run, such as /audit Only check the docs/ directory., and Claude Code appends it to the command.
Agent image
A group keeps its prompts as Claude Code commands in an image of its own, built on the agents image, with one Markdown file per prompt in /root/.claude/commands/:
FROM registry.gitlab.com/hosstio/docker-images/agents/main:claude
COPY prompts/ /root/.claude/commands/Then the group points the jobs at the image and its commands:
glab variable set AGENT_CLAUDE_IMAGE --group <group> --value registry.gitlab.com/<group>/agents/main:latest
glab variable set AGENT_PROMPT_REVIEW --group <group> --value /review
glab variable set AGENT_PROMPT_WORK --group <group> --value /workA job pulls an image from another project's registry with its job token, so that project lists the group in its job token allowlist (Settings > CI/CD > Job token permissions).
Give each command disable-model-invocation: true in its frontmatter, so Claude never starts one by itself, such as a merge command during a review. Claude Code doesn't expand a variable such as $CI_PROJECT_PATH in a command's file, so the image's agent instructions, such as ~/AGENTS.md, tell Claude to read such names with printenv, which every agent job allows.
Reports
claude:scheduled posts Claude's final answer to Slack. When the answer is only NO_REPORT, it posts nothing, so a prompt that ends with "When you changed nothing, reply with only NO_REPORT." keeps the runs that changed nothing out of the channel.
Schedules
claude:scheduled runs on every pipeline schedule that sets AGENT_SCHEDULED_PROMPT. The variable is the task: a command of the agent image, such as /update, or the prompt itself. A project can have several schedules with different prompts and cadences, and each schedule can set other variables of the job for its run, such as AGENT_CLAUDE_MODEL. A schedule runs on one branch, so develop and main can have schedules and prompts of their own; an update schedule on develop opens its merge requests against develop. A schedule without AGENT_SCHEDULED_PROMPT runs the pipeline without an agent. The job posts Claude's final answer to Slack (SLACK_MESSAGE_CHANNEL, the project name by default) unless the answer is only NO_REPORT (see Reports); a schedule that shouldn't post at all sets SLACK_AUTHENTICATION_TOKEN to an empty value.
With commands for these tasks in the agent image, a project's schedules can look like this:
| Schedule | AGENT_SCHEDULED_PROMPT | Cadence | Set alongside |
|---|---|---|---|
| Update | /update | Weekly or nightly | |
| Audit | /audit | Weekly | AGENT_CLAUDE_MODEL=opus for a deeper read, AGENT_SCHEDULED_REPOSITORIES for the related repositories |
| Backlog | /backlog | Daily | |
| Merge | /merge | Daily | AGENT_CLAUDE_MODEL=opus, AGENT_CLAUDE_EFFORT=xhigh |
| Status | /status | Weekly | AGENT_CLAUDE_MODEL=haiku, AGENT_CLAUDE_EFFORT=medium |
A merge prompt deploys in most projects, since merging deploys, under the name of the user behind AGENT_GITLAB_AUTH_TOKEN, so run it after the update and audit runs.
A scheduled pipeline runs the project's other jobs too, all of them except the deploys: linting, tests and image builds run next to the agent job, and pipelines/default.yaml adds pre-commit:autoupdate to every schedule. For an image project, that makes the nightly update also a nightly rebuild with fresh base packages.
Cost controls
Every invocation is capped three ways: a dollar budget (AGENT_CLAUDE_MAX_BUDGET_USD), a turn limit (AGENT_CLAUDE_MAX_TURNS), and a job timeout (15 minutes for review, 60 minutes for work, 2 hours for scheduled runs). claude:review runs Claude once per play, or once per push when it's automatic. claude:work runs it once per play, or once per new branch without changes. Claude fetches its own context with glab, so a run spends a few turns on reading before it starts on the task. Each job attaches a Claude report with the model, effort, turns, duration, cost and tokens of every run, which merge requests link as "Claude report"; see the Claude helper.