Vik does not talk to GitHub on its own. Everything that touches the
tracker — listing issues, reading details, leaving comments, moving
state — is something you tell Vik to do, either through the
issues.pull.command shell snippet or through commands written into
your stage prompt sources.
This guide shows the patterns we use in practice with the GitHub CLI
(gh) and jq. It assumes you have read Get Started
and have a working workflow.yml.
The GitHub CLI handles auth for you. Make sure it is logged in for the host you use:
gh auth status --active --hostname github.com
gh auth setup-git --hostname github.comAlternatives, in order of preference:
gh auth loginfor interactive setup (recommended).GH_TOKENorGITHUB_TOKENexported in the daemon's environment. Use a fine-grained token with the minimum scopes needed (issues,pull_requests, optionallycontents).
Never
echo,cat, or commit a token. If you setGH_TOKENin a shell file, make sure that file is git-ignored.
vik doctor only checks workflow.yml. It does not call GitHub —
your pull command and prompts are responsible for failing loudly when
auth is broken.
issues.pull.command is a shell command Vik runs on a loop. It must
print one JSON array of issue objects to stdout. Each issue must
include at least:
id— the GitHub issue number, as a string (Vik uses this for workspace folder names).title— the GitHub issue title.state— the value Vik will match againstissue.stages.<stage>.when.state. Match is case-sensitive.
GitHub does not have a built-in "workflow state" field. Use one of these conventions:
This is the pattern used by Vik's own workflow. You add labels like
todo, work, review to issues; the pull command picks the active
state label and emits it as state.
issues:
pull:
command: >-
gh issue list --label "vik" --state "open" --limit 50
--search 'label:todo,label:work,label:review -label:blocked sort:created-asc'
--json number,title,labels
--jq '
[
.[]
| ([.labels[].name]
| map(select(. == "todo" or . == "work" or . == "review"))
) as $states
| select($states | length == 1)
| { id: (.number | tostring), title: .title, state: $states[0] }
]
'
idle_sec: 5What this does, step by step:
gh issue listfilters to open issues with theviklabel.--searchfurther restricts to issues that carry exactly one of the workflow state labels and are not blocked.--jsonselects the raw fields we need.--jqreshapes each issue into Vik's required{id, title, state}shape, dropping any issue that has zero or more than one state label (which would be ambiguous).
If you use GitHub Projects (v2), pull from there instead so the project board is the source of truth:
gh project item-list <project-number> --owner <org> --format json --limit 100 \
| jq '
[
.items[]
| select(.content.type == "Issue")
| {
id: (.content.number | tostring),
title: .content.title,
state: .status
}
]
'Replace .status with whatever the field is called in your project.
You can find the exact key name with gh project field-list <number> --owner <org>.
- Limit the result set. Vik runs this every cycle.
--limit 50or a tight--searchquery keeps you well under GitHub's rate limit. - Sort deterministically (
sort:created-asc,sort:updated-desc, etc.) so the same issue is not "first" on every cycle if it matters to your hooks. - Test the command by hand. Run the exact string in your shell and confirm the output is a JSON array — not an object, not newline-delimited objects.
- Pick
idle_secto match your tracker. GitHub's secondary rate limit is generous for read-onlygh issue listcalls; 5–30 seconds is fine for personal use. Bigger orgs should go higher.
Stage prompts can render Vik template values directly:
If
issues.pull.commandreturned extra fields, for examplebranch, they are available as issue template values such as{{ issue.branch }}.
You are working on issue {{ issue.id }}: {{ issue.title }}.
State: {{ issue.state }}
Workdir: {{ issue.workdir }}
But the pull command only carries the small subset of fields you asked for. Anything richer — body, comments, attached PRs, reviewers — must be fetched fresh inside the prompt itself, because the issue may have moved by the time the agent runs.
gh issue view {{ issue.id }} \
--json number,title,body,state,labels,assignees,comments,url,updatedAtUseful JSON keys you can ask for:
number,title,body,urlstate(OPEN/CLOSED),labels,assignees,milestonecomments— full comment thread, withbody,author,createdAt.closingIssuesReferences— issues this issue closes.linkedBranches— branches GitHub auto-linked.projectItems— project board entries.
Any field listed in gh issue view --help works.
gh pr list --search "linked:{{ issue.id }} repo:owner/name" \
--state all --json number,title,state,isDraft,url,headRefNameOr, when your prompt opens a PR with Closes #{{ issue.id }}:
gh pr view <pr-number> \
--json number,title,state,isDraft,reviews,statusCheckRollup,mergeable,urlgh pr view <pr-number> --json reviews,reviewDecision
gh pr checks <pr-number>For inline review comments specifically (the line-by-line ones), use the API directly:
gh api repos/owner/name/pulls/<pr-number>/commentsVik never updates GitHub. Your prompt sources must include the exact commands the agent should run when it wants to move the issue forward. Pick the same convention you used in the pull command.
gh issue edit {{ issue.id }} --remove-label todo --add-label work
gh issue edit {{ issue.id }} --remove-label work --add-label reviewAlways remove the previous state label and add the new one in a single call. Otherwise the issue may briefly carry both, and the pull command's "exactly one state label" filter will skip it.
gh project item-edit \
--id <item-id> \
--field-id <status-field-id> \
--project-id <project-id> \
--single-select-option-id <option-id>The IDs are stable per project; cache them in environment variables or as a small helper script that the prompt can call.
The cleanest way is to let GitHub close it through a PR closing
keyword. In the prompt, instruct the agent to add Closes #{{ issue.id }} to the PR body. When the PR merges, the issue
auto-closes. No explicit gh issue close needed.
Manual close, when you need it:
gh issue close {{ issue.id }} --reason completedgh issue comment {{ issue.id }} --body "Plan posted; moving to work."For multi-line bodies, write to a temp file first:
cat > /tmp/comment.md <<'EOF'
## Plan
1. Step one
2. Step two
EOF
gh issue comment {{ issue.id }} --body-file /tmp/comment.mdgh does not have a one-line edit, but the API does:
gh api -X PATCH repos/owner/name/issues/comments/<comment-id> \
-f body="$(cat /tmp/updated.md)"git push -u origin HEAD
gh pr create \
--title "<short title>" \
--body "Closes #{{ issue.id }}
...details..." \
--label vikgh issue list already excludes PRs. If you use gh search issues,
add is:issue to the query so PR results do not leak in.
# 1. Pull command prints a JSON array, not an error or empty string.
gh issue list --label vik --state open --json number,title,labels \
| jq 'length'
# 2. Auth works for issue + PR write.
gh issue edit <test-issue> --add-label vik && \
gh issue edit <test-issue> --remove-label vik
# 3. Vik schema is happy.
vik doctor ./workflow.yml