Everything a task and its project can declare, and what Gardener generates from them. For how to
write a task, see Creating tasks. Tables marked generated are produced from
the code, and pnpm check fails if they fall out of date.
.gardener/
gardener.json project settings
gardener.lock.json generated: compiled bundles and their hashes
SKILL.md generated: a task-writing guide for coding agents
tasks/<dir>/TASK.md one task per directory
.github/workflows/
gardener-<id>.yml generated: one caller workflow per task
gardener-sync.yml generated: syncs tasks from the default branch, checks pull requests
gardener init creates the project; gardener generate writes every generated file. Never edit a
generated file: the sync and the pull request check both fail if one differs from what generate
produces. generate refuses to overwrite a workflow it did not write.
| Key | Required | Meaning |
|---|---|---|
schemaVersion |
yes | gardener.project/v1. |
target |
yes | github-actions/v1, the only target. |
release.workflowRef |
yes | The Gardener release this project uses: scuffi/gardener/.github/workflows/gardener-task.yml@<commit>. Set by init and moved by upgrade. |
handle |
no | The GitHub handle people @mention to reach this project's tasks, without @. mentions: [self] resolves to it. Pick one that is not a real account (gh api users/<name> should return 404), or every mention also notifies that person. |
syncWorkflow |
no | The generated sync workflow's file name, when gardener-sync.yml is taken: a gardener-*.yml name of at most 64 characters. |
A key this release does not know fails with a message to upgrade: the sync and check workflows run the pinned release's parser.
YAML frontmatter followed by a Markdown body. The body is the model's instructions; it cannot grant tools, effects, network access or permissions.
| Key | Required | Meaning |
|---|---|---|
schema |
yes | gardener.task/v1. |
id |
yes | Immutable identity, independent of the directory: lowercase letters, digits, ., _ and -, up to 160 characters. Names the workflow gardener-<id>.yml. |
name |
yes | Display name, up to 100 characters. |
description |
yes | Up to 1,000 characters. |
trigger or triggers |
exactly one | One trigger, or a list. See Triggers. |
tools |
yes | At least one. See Tools. |
effects |
no | What the task may change. Omit or [] for a read-only task. See Effects. |
network |
yes | Egress posture. See Network. |
limits |
yes | Run budget. See Limits. |
model |
no | Model ID. Defaults to "@cf/zai-org/glm-5.3". See Model. |
draft |
no | true makes the task run only by hand. |
checkout |
no | pull-request-head checks out the pull request's head instead of GitHub's merge preview, also for a comment on a pull request or a manual run naming one. Needs a pull request or issue comment trigger. Implied by commit.merge_base, and when a commit.create entry lists branches other than gardener/**. |
reactions |
no | false turns off the 👀 → 🚀/😕 reactions. On by default. See Reactions. |
effect-scope |
no | trigger confines every run to what triggered it, whoever started it. Without it, only runs an untrusted starter caused are confined. See Effect scope. |
Each trigger is an event plus the filters its kind accepts. A task may use each kind at most once.
Every task can also be run by hand: generate adds github.workflow_dispatch when it is not
declared. Using a filter a kind does not accept is a compile error.
| Kind | Filters |
|---|---|
github.issue.opened |
labels-all, mentions, authors |
github.issue.edited |
labels-all, mentions, authors |
github.issue.labeled |
labels-all, opened-by |
github.issue.unlabeled |
labels-all, opened-by |
github.issue.reopened |
labels-all, opened-by |
github.issue_comment.created |
labels-all, mentions, authors, opened-by |
github.issue_comment.edited |
labels-all, mentions, authors, opened-by |
github.pull_request.opened |
labels-all, mentions, authors |
github.pull_request.reopened |
labels-all, opened-by |
github.pull_request.synchronize |
labels-all, opened-by |
github.pull_request.ready_for_review |
labels-all, opened-by |
github.pull_request.converted_to_draft |
labels-all, opened-by |
github.pull_request.edited |
labels-all, mentions, authors |
github.pull_request.labeled |
labels-all, opened-by |
github.pull_request.unlabeled |
labels-all, opened-by |
github.pull_request_review.submitted |
labels-all, mentions, authors, opened-by |
github.pull_request_review_comment.created |
labels-all, mentions, authors, opened-by |
github.pull_request_review_comment.edited |
labels-all, mentions, authors, opened-by |
github.push |
branches (required) |
github.workflow_dispatch |
subject |
github.schedule |
cron (required) |
github.discussion.created |
labels-all, mentions, authors |
github.discussion.edited |
labels-all, mentions, authors |
github.discussion.answered |
labels-all, opened-by |
github.discussion.unanswered |
labels-all, opened-by |
github.discussion.labeled |
labels-all, opened-by |
github.discussion.unlabeled |
labels-all, opened-by |
github.discussion_comment.created |
labels-all, mentions, authors, opened-by |
github.discussion_comment.edited |
labels-all, mentions, authors, opened-by |
| Filter | Meaning |
|---|---|
labels-all |
Every listed label must be on the issue, pull request or discussion. Up to 20. On the three .labeled triggers, the label just added must also be one of them, so a later unrelated label doesn't start the task again. With N listed labels up to N events can match (one per listed label as it is added); with one label, exactly once. |
mentions |
The text must @mention one of these handles; self is the project's handle. Whole-handle and case-insensitive. On comment triggers the text is the comment, on pull_request_review.submitted the review, otherwise the issue, pull request or discussion body. On an .edited trigger, only an edit that adds a mention counts. |
authors |
Who may have written the text: maintainers, any, or a list of logins that may include maintainers. maintainers is the owner, organization members and collaborators, or anyone with write or admin access. The default is maintainers for comment triggers, pull_request_review.submitted, and any trigger with mentions; otherwise any. |
opened-by |
Exact logins that opened the thread the event belongs to, such as ["dependabot[bot]"]. |
branches |
Branch patterns a push must match. Required on github.push. |
cron |
A five-field cron expression separated by single spaces, using only numbers, *, ranges, lists and steps: no names such as MON, and no ?, L, W, # or @daily. Required on github.schedule. A task has at most one. |
subject |
On github.workflow_dispatch only: issue or pull_request, offered as a manual run's target input whatever else the task triggers on. For a task whose only trigger is manual, such as one another task starts with task.run. |
Pull request events from forks never run, and pull_request_target cannot be declared. Manual runs
ignore labels-all, mentions and authors.
| Tool | Meaning |
|---|---|
repository.list_files |
List files in the checkout. |
repository.read_file |
Read a file in the checkout. |
repository.exec |
Run shell commands in the checkout, with unrestricted network access. See Network. |
repository.merge_base |
Write the merge of the pull request's base branch, prepared before the model ran, into the checkout with conflicts marked. Never commits; commit.merge_base does, and needs this tool. |
provider.api.read |
Read the GitHub API: REST GET/HEAD and GraphQL queries. |
No tool ever holds a token that can write. provider.api.read is served by the bridge; its token is
never shown to the model or to commands.
effects lists the exact operations a task may propose, and message.send (see
Messages) and task.run (see Starting other tasks). Each entry is a kind, a family glob, or a kind with options. Every declared effect is applied automatically, with no approval step.
| Glob | Expands to |
|---|---|
check.* |
check.rerun |
discussion.* |
discussion.comment.create, discussion.comment.update, discussion.answer.mark, discussion.answer.unmark, discussion.close, discussion.reopen |
git.* |
branch.create, commit.create, commit.merge_base (only for tasks that declare the repository.merge_base tool) |
issue.* |
issue.label.add, issue.label.remove, issue.comment.create, issue.comment.update, issue.close, issue.reopen, issue.assignee.add, issue.assignee.remove, issue.create |
pull_request.* |
pull_request.comment.create, pull_request.comment.update, pull_request.review.submit, pull_request.reviewer.request, pull_request.reviewer.remove, pull_request.update, pull_request.label.add, pull_request.label.remove, pull_request.update_branch, pull_request.review_comment.reply, pull_request.review_thread.resolve, pull_request.open, pull_request.open_draft, pull_request.merge |
release.* |
release.create, release.update, release.publish, release.delete |
A family glob grants every current and future member of its family. Prefer exact kinds: git.*,
pull_request.* (which includes pull_request.merge), release.* (which includes
release.delete) and check.* all grant more than most tasks need.
| Kind | Does | Write scopes | Outputs |
|---|---|---|---|
issue.label.add |
Add an existing label to an issue. | issues |
issueNumber, label |
issue.label.remove |
Remove a label from an issue. | issues |
issueNumber, label |
issue.comment.create |
Comment on an issue. | issues |
commentId, commentUrl, issueNumber |
issue.comment.update |
Edit a comment on an issue. | issues |
commentId, commentUrl, issueNumber |
issue.close |
Close an issue, as completed or not planned (reason). |
issues |
issueNumber, issueUrl, state |
issue.reopen |
Reopen an issue. | issues |
issueNumber, issueUrl, state |
issue.assignee.add |
Assign someone to an issue. | issues |
assigneeId, assigneeLogin, issueNumber |
issue.assignee.remove |
Unassign someone from an issue. | issues |
assigneeId, assigneeLogin, issueNumber |
issue.create |
Open an issue, optionally with labels and assignees. | issues |
issueNumber, issueUrl |
pull_request.comment.create |
Comment on a pull request's conversation. | pull-requests |
commentId, commentUrl, pullNumber |
pull_request.comment.update |
Edit a comment on a pull request's conversation. | pull-requests |
commentId, commentUrl, pullNumber |
pull_request.review.submit |
Submit a review, optionally with line comments. | pull-requests |
pullNumber, reviewId, reviewState, reviewUrl |
pull_request.reviewer.request |
Request reviewers. | pull-requests |
pullNumber |
pull_request.reviewer.remove |
Remove requested reviewers. | pull-requests |
pullNumber |
pull_request.update |
Change a pull request's title, body, state or draft status. | pull-requests |
draft, pullNumber, pullUrl, state, title |
pull_request.label.add |
Add an existing label to a pull request. | pull-requests |
label, pullNumber |
pull_request.label.remove |
Remove a label from a pull request. | pull-requests |
label, pullNumber |
pull_request.update_branch |
Update a pull request with its base, by rebase or merge. | contents, pull-requests |
pullNumber |
pull_request.review_comment.reply |
Reply in a review thread on the triggering pull request. | pull-requests |
commentId, commentUrl, pullNumber |
pull_request.review_thread.resolve |
Resolve a review thread on the triggering pull request. | pull-requests |
pullNumber, threadId |
branch.create |
Create a branch. | contents |
branch, branchUrl, commitSha, ref |
commit.create |
Commit the files the task changed in its checkout. | contents |
branch, commitSha, commitUrl, parentSha, treeSha |
commit.merge_base |
Add a merge commit to the triggering pull request's branch that brings its base branch in; never rewrites history. | contents |
baseSha, branch, commitSha, commitUrl, pullNumber, treeSha |
pull_request.open |
Open a pull request, ready for review. | pull-requests |
baseRef, headRef, headSha, pullNodeId, pullNumber, pullUrl |
pull_request.open_draft |
Open a draft pull request. | pull-requests |
baseRef, headRef, headSha, pullNodeId, pullNumber, pullUrl |
pull_request.merge |
Merge a pull request after verifying its checks. | contents, pull-requests |
mergeCommitSha, pullNumber, pullUrl |
discussion.comment.create |
Comment on a discussion. | discussions |
commentId, commentNodeId, commentUrl, discussionNumber |
discussion.comment.update |
Edit a comment on a discussion. | discussions |
commentId, commentNodeId, commentUrl, discussionNumber |
discussion.answer.mark |
Mark a comment as the discussion's answer. | discussions |
answerCommentId, discussionNumber |
discussion.answer.unmark |
Unmark a discussion's answer. | discussions |
answerCommentId, discussionNumber |
discussion.close |
Close a discussion. | discussions |
discussionNumber, discussionUrl, state |
discussion.reopen |
Reopen a discussion. | discussions |
discussionNumber, discussionUrl, state |
check.rerun |
Re-run a GitHub Actions check run. | checks |
checkRunId, headSha, status |
release.create |
Create a release. | contents |
draft, prerelease, releaseId, releaseUrl, tagName |
release.update |
Edit a release. | contents |
draft, prerelease, releaseId, releaseUrl, tagName |
release.publish |
Publish a draft release. | contents |
draft, prerelease, releaseId, releaseUrl, tagName |
release.delete |
Delete a release. | contents |
releaseId, tagName |
message.send |
Post one message about an issue, pull request or discussion to the repository's chat space, after every other step applied. At most one per run. | none | none |
task.run |
Start another task of this repository on this run's issue or pull request, or on a pull request the plan opens, after every other step applied. At most one per run, always the last step, never retried. | actions |
none |
A step can use an earlier step's outputs, either as a whole field or as {{name}} placeholders in
text. See Using one step's result in a later step.
message.send posts one short message about an issue, pull request or discussion to the
repository's chat space (a Google Chat or Slack incoming webhook in the GARDENER_CHAT_WEBHOOK
repository secret). It is declared in effects like an operation, but it is not one:
- it must be named exactly;
message.*is not a glob, and no family glob includes it; - a run sends at most one, after every other step applied; it is not counted by
max-effect-operationsormax-effect-bytes, publishes no outputs and takes no references; - the model writes only a summary of at most 400 characters, one line of plain text without
<,>,&or backticks; Gardener adds an emoji from the subject's labels, the repository name, the link, the author and the title (see Messages for the format); - it needs no write scope; the caller grants read on issues, pull requests and discussions so apply can check the subject has not changed since the model read it, and sends nothing if it has;
- under trigger scope it may only be about what triggered the run;
- a task may not declare both
message.sendandrepository.exec.
Apply records what became of the message in the plan's receipt, never the destination or the text:
| Status | Reason | Meaning |
|---|---|---|
sent |
The chat service accepted it. | |
skipped |
destination_unset, destination_unsupported |
GARDENER_CHAT_WEBHOOK is empty, or not an https://chat.googleapis.com/ or https://hooks.slack.com/ URL. The job succeeds. |
conflicted |
subject_changed, subject_elsewhere |
The subject changed since the model read it (other than by this run's own steps), or its link is outside the repository. Nothing is sent; the job succeeds. |
failed |
subject_unreadable, send_failed, deadline_expired |
The job fails after the operations applied. A rerun tries only the message again. |
A plan's Google Chat message reaches the space at most once: each attempt carries the same messageId, and a
409 ALREADY_EXISTS refusal of a repeat counts as sent. Slack delivery is at least once: a rerun
after a lost answer can post the message twice. An issue subject whose link is a pull request is
recorded as pull_request.
task.run starts another task of the same repository once every other step applied. Like
message.send it is declared in effects but is not an operation. It always lists the tasks it may
start:
effects:
- issue.label.add
- task.run: { tasks: [fix] }- The model names one listed task and a subject, and nothing else: no inputs, prompt, ref or
workflow. The subject is this run's own issue or pull request (
issueNumberorpullNumber, matched by kind and number), or a pull request an earlierpull_request.openorpull_request.open_draftstep of the plan opens, by referencing itspullNumber. - At most one per run, and it must be the last step. It is not counted by
max-effect-operationsormax-effect-bytesand publishes no outputs. - When the model adds it, the Worker resolves the task against the repository's enabled tasks
(as last synced) and checks its workflow takes the subject as a dispatch input: a trigger on
issues or pull requests, or
github.workflow_dispatchwithsubject. It checks again when apply dispatches. The workflow is the target's generatedgardener-<id>.yml, named by the Worker. - Apply dispatches that workflow on the default branch with one input,
issueorpull_request, so the started run reads its subject fresh. It recordsdispatchingbefore calling GitHub and never dispatches twice: a rerun afterdispatchingsettles asdispatch_unknownwithout calling GitHub, and the job fails so someone can check whether the task started and run it by hand. - The caller job of a task that declares it is granted
actions: write. See SECURITY.md for what else that allows. generaterefuses a target that is not a task of the project, the task itself, a target whose workflow takes no input the task can name, a cycle of tasks starting each other, and a pin whose apply predatestask.run(0.1.20 or earlier).
The started run is matched against a record the Worker wrote when apply reported dispatching.
It inherits the chain's root starter decision, never a wider one, so a chain an outsider's issue
started stays confined to that subject at every depth, and every run in it counts against the
root author's runs-per-author (and each task's runs). A run started by a person or an event is
depth 0; a run at depth 3 may not start another. A manual run dispatched by a bot that matches no
record, or one that carries a prompt, fails before the model; a person's Run workflow is
unaffected. See Lineage and
Bot dispatches.
| Status | Reason | Meaning |
|---|---|---|
dispatching |
Apply is about to call GitHub. Never retried from here. | |
dispatched |
GitHub accepted the dispatch. | |
dispatch_unknown |
dispatch_refused, dispatch_failed, interrupted |
GitHub refused it, the call failed, or an attempt stopped after dispatching. The job fails; it is not dispatched again. |
branch.create, commit.create, pull_request.open and pull_request.open_draft may only use
branches under gardener/ unless their entry lists branches:
effects:
- kind: commit.create
branches: ["gardener/**", "docs/*"]The list replaces the default. * matches within one path segment and ** one or more whole
segments. A pattern never matches the default branch; name it exactly to allow it. Quote patterns in
YAML.
- Labels and assignees must already exist or be assignable. If GitHub drops one, the step fails and names it.
issue.createmarks the issue it opens, so a resumed run finds it among the 500 newest issues and pull requests instead of opening a second one.pull_request.openand.open_draftcan take up to tenlabels, which needspull_request.label.addtoo.pull_request.update_branchbrings a pull request up to date with its base, byrebaseormerge, only if the head is the one the task saw. It stops on conflicts.pull_request.review_comment.replyand.review_thread.resolveact only on the pull request that triggered the run.commit.createcommits the files the task changed in its checkout, on a branch that points at the checked-out commit. After it pushes to a pull request's branch, later comment, reply and resolve steps in the same plan accept the new head.pull_request.mergeverifies exactly the checks the step lists before merging.- Every step checks the state it was planned against (a head SHA, an issue state, an update time) and refuses if it changed. Apply stops at the first failed step and resumes from it.
A committed change may not write under .git/, .github/workflows/, .github/actions/ or
.gardener/, nor any CODEOWNERS file or .github/dependabot.yml/.yaml, so a run can never
rewrite what governs the next run.
| Task | Required network |
|---|---|
Declares repository.exec |
default: allow, allow: [], deny: [] |
Does not declare repository.exec |
default: deny, allow: [], deny: [] |
Gardener does not sandbox the runner, so repository.exec can reach any host and could send private
source anywhere. Host lists are rejected, because this target cannot enforce them. generate warns
about every task that declares repository.exec.
| Key | Required | Meaning | Allowed |
|---|---|---|---|
runtime-seconds |
yes | Wall-clock limit for the run. | 30–21,000 |
max-turns |
yes | Model responses. | at least 3 |
max-tool-calls |
yes | Tool calls, including the final finish_task. |
at least 3 |
input-tokens |
yes | Largest single model request. The conversation is resent each turn. | any positive |
output-tokens |
yes | Total generated across the run, including reasoning. | at least 16 × max-turns |
max-effect-operations |
no | Most operations one plan may propose. Needs at least one effect. | 1–1,000 |
max-effect-bytes |
no | Largest plan. | 1,024–50,000,000 |
runs-per-author |
no | Runs one untrusted starter may cause, in any window of that length. Defaults to 3/day. |
<count>/<minute|hour|day|week|month> with a count of 1–99,999, or unlimited |
runs |
no | Runs of the task by anyone, maintainers included, in any window of that length. No default. | <count>/<minute|hour|day|week|month> with a count of 1–99,999 |
A run that exceeds a limit fails with budget-exceeded and changes nothing. Every plan and receipt
must also fit in 4 MiB of canonical JSON, and one commit may change at most 1,000 files. The plan
job times out after runtime-seconds plus 10 minutes.
A run over runs-per-author or runs completes as a skip without calling the model; a month is 30
days. See Run limits. To cap model use across every task, set a rate
limit on the installation's AI Gateway; see Capping model use.
| Prefix | Request format |
|---|---|
@cf/… |
Workers AI, OpenAI-compatible chat completions |
openai/… |
OpenAI Responses |
anthropic/… |
Anthropic Messages |
| anything else | OpenAI-compatible chat completions |
Quote @cf/ IDs in YAML. generate warns about other prefixes, which may not support tool calls.
Non-Cloudflare models need provider keys or Unified Billing on the installation's AI Gateway.
| Input | Offered when | Meaning |
|---|---|---|
prompt |
always | Extra instructions for this run, up to 20,000 characters. |
issue |
the task has an issue trigger, or github.workflow_dispatch with subject: issue |
Issue number to run against. |
pull_request |
the task has a pull request trigger, or github.workflow_dispatch with subject: pull_request |
Pull request number to run against. |
Give at most one target. Start a run from the workflow's Run workflow button or with
gh workflow run gardener-<id>.yml. A manual run is trusted when a person starts it; one a bot or
App token dispatches is admitted only as a run another task started with task.run, and never
takes a prompt.
| Outcome | Reaction |
|---|---|
| Running | 👀 |
| Plan applied | 🚀 |
| Planning or applying failed | 😕 |
| Nothing proposed, skipped or cancelled | none |
Reactions go on the issue, pull request, comment or discussion that started the run, and only for
authors the task admits; maintainers is approximated by GitHub's author_association, so a
private organization member may get none. Review submissions, schedules, pushes and manual runs
get none. Projects pinned to 0.1.12 or earlier get none.
Each task's workflow calls Gardener's reusable workflow at the release's commit. Its jobs:
| Job | Checkout | Runs | Token |
|---|---|---|---|
| Plan | yes | the model and its tools | read-only, declared on the job |
| React, React with result | no | fixed reaction API calls | the caller's grant |
| Apply | no | the exact plan the runtime validated | the caller's grant |
The caller grants the union the jobs need: the planning job's fixed read scopes (checks,
contents, discussions, issues, pull-requests, statuses, plus id-token: write), the write
scopes of the declared effects, and the reaction scopes (issues, pull-requests or discussions
write, depending on the triggers). That union is the task's maximum authority.