github: support schedule events
note: draft for now as I want to make sure we're happy with this / think through it over the weekend.
Adds support for schedule events in the GitHub Action, allowing users to run OpenCode on a cron schedule.
- add
scheduleto allowed event types - skip
/opencodekeyword check for schedule events (no comment to parse) - skip creating issue comment for schedule events (no issue/PR context)
-
promptinput is required for schedule events - branch naming:
opencode/scheduled-{6-random-hex-chars}-{timestamp} - omit co-authored-by on commits (schedule operates as the repo)
- add "Supported Events" section to docs with table and example workflow
Users can now create scheduled workflows that invoke OpenCode with a prompt to perform automated tasks like code reviews, reports, or maintenance. OpenCode can still open PRs/issues and call tools (webhooks, etc.) as needed.
I also tried to keep this pretty compact — most of the scaffolding was there, but b/c scheduled events don't get comment bodies + can't work on branches needed to make sure that was covered.
call Stack: schedule vs issue_comment
┌─────────────────────────────────────────────────────────────────────────────────┐
│ GITHUB ACTION ENTRY │
│ github/action.yml → opencode github run │
│ packages/opencode/src/cli/cmd/github.ts │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ context.eventName check (lines 391-398) │
│ Allowed: "issue_comment" | "pull_request_review_comment" | "schedule" │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ isScheduleEvent (line 390) │
│ const isScheduleEvent = context.eventName === "schedule" │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ getUserPrompt() │
├─────────────────────────────────────┬───────────────────────────────────────────┤
│ SCHEDULE │ ISSUE_COMMENT │
├─────────────────────────────────────┼───────────────────────────────────────────┤
│ prompt = process.env["PROMPT"] │ prompt = payload.comment.body │
│ ├─ ✗ FAIL: !prompt → throw │ + image processing │
│ └─ ✓ PASS: return { userPrompt } │ └─ returns { userPrompt, promptFiles } │
└─────────────────────────────────────┴───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ configureGit() │
│ (same for both events) │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ if (!isScheduleEvent) { assertPermissions(); addReaction() } │
├─────────────────────────────────────┬───────────────────────────────────────────┤
│ SCHEDULE │ ISSUE_COMMENT │
├─────────────────────────────────────┼───────────────────────────────────────────┤
│ skipped (no actor/comment) │ checks collaborator permission │
│ │ adds 👀 reaction to comment │
└─────────────────────────────────────┴───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Session.create() + subscribeSessionEvents() │
│ (same for both) │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ MAIN EVENT BRANCHING │
│ if (isScheduleEvent) { ... } │
│ else if (isPullRequest) { ... } │
│ else { /* issue */ } │
└─────────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────────┴───────────────────────────────┐
▼ ▼
┌───────────────────────────────────┐ ┌───────────────────────────────────┐
│ SCHEDULE │ │ ISSUE_COMMENT │
├───────────────────────────────────┤ ├───────────────────────────────────┤
│ │ │ │
│ checkoutNewBranch("schedule") │ │ checkoutNewBranch("issue") │
│ → opencode/scheduled-{hex}-{ts} │ │ → opencode/issue{N}-{ts} │
│ │ │ │
│ chat(userPrompt, []) │ │ fetchIssue() │
│ │ │ buildPromptDataForIssue() │
│ │ │ chat(userPrompt + data, files) │
│ │ │ │
├───────────────────────────────────┤ ├───────────────────────────────────┤
│ if (dirty) { │ │ if (dirty) { │
│ summarize() │ │ summarize() │
│ pushToNewBranch(isSchedule=✓) │ │ pushToNewBranch(isSchedule=✗) │
│ └─ NO co-author line │ │ └─ WITH co-author line │
│ createPR() ✅ │ │ createPR() │
│ console.log(PR #) │ │ createComment(PR #) │
│ } else { │ │ removeReaction() │
│ console.log(response) │ │ } else { │
│ } │ │ createComment(response) │
│ │ │ removeReaction() │
│ // NO createComment │ │ } │
│ // NO removeReaction │ │ │
└───────────────────────────────────┘ └───────────────────────────────────┘
Failure Modes
| Stage | Schedule Event | Issue Comment Event |
|---|---|---|
eventName check |
✓ passes | ✓ passes |
getUserPrompt |
✗ fails if PROMPT env missing |
✗ fails if no /oc or /opencode in body |
assertPermissions |
skipped | ✓ typically passes (human actor) |
addReaction |
skipped | ✓ adds 👀 to comment |
chat |
✗ fails on LLM/tool errors | ✗ fails on LLM/tool errors |
pushToNewBranch |
✗ fails on git push errors | ✗ fails on git push errors |
createPR |
✅ can create PRs | ✅ can create PRs |
createComment |
skipped | ✗ fails on API error |
removeReaction |
skipped | ✓ removes 👀 from comment |
Key Differences
| Aspect | Schedule | Issue/PR Comment |
|---|---|---|
| Prompt source | PROMPT env var (required) |
Comment body |
| Branch name | opencode/scheduled-{6-char-hex}-{timestamp} |
opencode/issue{N}-{timestamp} |
| Co-authored-by | Omitted (schedule operates as repo) | Included (credits human actor) |
| Output | Console logs | GitHub comment |
| Reactions | None | 👀 added/removed |
| Permission check | Skipped | Required |
User elithrar does not have write permissions
github run
OK, I think I’m happy with this after staring at it again last night.
/review
lgtm
oh @elithrar I should really clean this up but rn github/index.ts does nothing, rn all the github workflow stuff is in ...../cmd/github.ts
At one point we meant to move it to be fully contained in the action and that's why github/index.ts exists but we never finished the migration and now it's just sitting there, so youll probably want to make changes to cmd/github.ts instead
Unless this changed recently ig, but last I checked that was the case still...
Yeah appears to still use github cmd: https://github.com/sst/opencode/blob/ad6a5e6157ea728d3d2cb29297d6bbb5f79dc6bc/github/action.yml#L66
ah duh — all the other PRs I've made have been against the CLI imp. Fixing.
fixed + upgraded diagram to show a couple of tweaks
/review
Updated this and re-reviewed.
If we add more events I want to re-think how we can make the active user event vs repo driven event control flow easier to follow.
On Sat, Dec 20, 2025 at 16:38 github-actions[bot] @.***> wrote:
@.**[bot] commented on this pull request.
In packages/opencode/src/cli/cmd/github.ts https://github.com/sst/opencode/pull/5810#discussion_r2637382503:
reaction_id: eyesReaction.id,}) } async function createComment(body: string) {
// Only called for non-schedule events, so issueId is defined console.log("Creating comment...") return await octoRest.rest.issues.createComment({Potential bug: The comment says "Only called for non-schedule events", but the catch block in the main try/catch (around line 536-542) calls createComment() unconditionally on error, which would fail for schedule events since issueId is undefined.
You may want to wrap the catch block calls to createComment() and removeReaction() with if (!isScheduleEvent) to prevent runtime errors during scheduled runs.
— Reply to this email directly, view it on GitHub https://github.com/sst/opencode/pull/5810#pullrequestreview-3601326780, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAEQ4GGMIXKM4E5UWFXL5T4CW6XDAVCNFSM6AAAAACPSMSIDCVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZTMMBRGMZDMNZYGA . You are receiving this because you were mentioned.Message ID: @.***>
/review
Code review complete. Found one potential bug (commented inline). Otherwise the code follows the style guide - uses early returns where appropriate, keeps functions together, and doesn't introduce unnecessary else statements or excessive nesting. The changes look good overall.
Reviewed and addressed the following for schedule event handling:
- Skip
assertPermissionsfor schedule events (avoidsgithub-actions[bot]actor failure) - Fix
summarize()fallback to avoid accessing undefinedpayloadfor schedule events - Verified all
issue.title,issue.body,pr.title,pr.bodycallsites are guarded