
TL;DR
The most common trigger for an AI coding agent is not a clock, it is an event. A GitHub webhook, a Railway service, and OpenCode headless add up to a repo where a labeled issue gets a real pull request without anyone at the keyboard. The full build, start to finish.
Scheduled agents have a sibling that gets far less attention: event-driven agents. A cron job answers "every day at 07:00", but most of the work an agent should take off your plate is not on a clock. An issue is opened. A bug report lands. A dependency bumps a major version. Each one is an event, and events deserve a webhook, not a schedule.
This guide builds the canonical version end to end: a repo where opening an issue with the agent label triggers a real coding agent that investigates, implements the smallest fix, runs your test suite, and opens a pull request. The receiving end is a small webhook service on Railway, and the worker is OpenCode in headless mode, the same opencode run pattern from our cron automation guide. We run a version of this shape for parts of this site; the mechanics below are the portable core. Seven steps, under an hour, every step ending in something you can run.
| Resource | Description |
|---|---|
| OpenCode CLI docs | opencode run, headless mode, agent and model flags |
| GitHub webhook docs | Events, payloads, delivery headers, signature validation |
| GitHub webhook best practices | The 10-second response rule and why you need a queue |
| Railway Services | Deploying a service from a GitHub repo or Dockerfile |
| Railway Public Networking | Getting your .railway.app domain and SSL |
| Railway Variables | Secrets and configuration for your service |
| Railway Pricing | Plans, included usage, and per-resource rates |
Prerequisites: a GitHub repo you own (a throwaway one is ideal for the first run), a free Railway account (new accounts get a one-time $5 grant, which covers this build several times over), and an API key for an LLM provider.
Install OpenCode locally with the official one-liner from the OpenCode docs:
curl -fsSL https://opencode.ai/install | bash
Authenticate a provider (opencode auth login), then confirm the single capability the whole pattern depends on - running one task and exiting, no TUI, no interaction:
opencode run --model opencode/deepseek-v4-flash "print the current directory tree, two levels deep"
If that prints a tree and returns cleanly, you have a worker. The DeepSeek V4 Flash 0731 release is the current budget sweet spot for this job at $0.14/$0.28 per million tokens; scheduled and event-driven work is exactly where cheap models earn their keep, with a test gate catching misses. OpenCode reads provider API keys from your environment, which is what lets the same setup work on a server with zero interactive login.
What you have now: a proven headless agent command that will run anywhere.
The receiver is deliberately boring: standard library only, two routes, no framework. It does three things: verify the request is really from GitHub, acknowledge it instantly, and hand the payload to the runner in the background.
// server.js
const http = require("node:http");
const crypto = require("node:crypto");
const { spawn } = require("node:child_process");
const SECRET = process.env.GITHUB_WEBHOOK_SECRET;
const TRIGGER_LABEL = process.env.TRIGGER_LABEL || "agent";
function signatureMatches(rawBody, header) {
if (!header || !SECRET) return false;
const expected =
"sha256=" +
crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
http
.createServer((req, res) => {
if (req.method === "GET" && req.url === "/healthz") {
res.writeHead(200);
return res.end("ok");
}
if (req.method !== "POST" || req.url !== "/webhook") {
res.writeHead(404);
return res.end();
}
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const raw = Buffer.concat(chunks).toString();
if (!signatureMatches(raw, req.headers["x-hub-signature-256"])) {
res.writeHead(401);
return res.end("bad signature");
}
const event = JSON.parse(raw);
const isNewAgentIssue =
event.action === "opened" &&
(event.issue?.labels || []).some((l) => l.name === TRIGGER_LABEL);
// GitHub expects a 2xx within 10 seconds. Acknowledge now,
// run the agent in the background.
res.writeHead(202);
res.end("accepted");
if (!isNewAgentIssue) return;
const child = spawn("bash", ["agent.sh"], {
detached: true,
stdio: "ignore",
env: {
...process.env,
REPO: event.repository.full_name,
DEFAULT_BRANCH: event.repository.default_branch,
ISSUE_NUMBER: String(event.issue.number),
ISSUE_TITLE: event.issue.title,
ISSUE_BODY: event.issue.body || "",
},
});
child.unref();
});
})
.listen(process.env.PORT || 3000);
Three details in this file are the security story:
X-Hub-Signature-256 header. Comparing the digest over the raw body with crypto.timingSafeEqual is the documented validation pattern, and it stops random internet traffic from spending your tokens.$(rm -rf /) is data, not a command.agent trigger a run. A typo-filled bug report from a stranger costs you nothing.What you have now: a receiver that can only ever do nothing or spawn a worker. Test it locally with PORT=3000 GITHUB_WEBHOOK_SECRET=test node server.js and a signed curl once Step 3 exists.
From the archive
Aug 5, 2026 • 11 min read
Aug 5, 2026 • 7 min read
Aug 5, 2026 • 7 min read
Aug 5, 2026 • 6 min read
One script, six moves: lock against duplicates, fresh clone, one bounded agent run, a test gate, a PR, a comment on the issue. This is the agent-chore.sh pattern from the cron guide adapted for events instead of a schedule.
#!/bin/bash
# agent.sh - runs from the webhook receiver
set -eu
: "${REPO:?}"; : "${ISSUE_NUMBER:?}"; : "${GITHUB_TOKEN:?}"
BRANCH="auto/issue-${ISSUE_NUMBER}"
export GH_TOKEN="$GITHUB_TOKEN"
gh auth setup-git
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
# One run per issue: if the branch exists, a previous run is handling it.
if git ls-remote --heads "https://github.com/${REPO}.git" "refs/heads/${BRANCH}" | grep -q .; then
exit 0
fi
git clone --depth=1 "https://github.com/${REPO}.git" "$WORK"
cd "$WORK"
git checkout -b "$BRANCH"
PROMPT="Work on this GitHub issue: https://github.com/${REPO}/issues/${ISSUE_NUMBER}
Title: ${ISSUE_TITLE}
Body:
${ISSUE_BODY}
Implement the smallest complete fix. Run the project's tests before finishing.
Change nothing unrelated to the issue."
timeout 900 opencode run --model "${AGENT_MODEL:-opencode/deepseek-v4-flash}" "$PROMPT"
# Nothing changed? Quiet exit.
git diff --quiet && git diff --cached --quiet && exit 0
# The gate. The agent's opinion of its own work does not count.
if [ -f package.json ]; then npm test; fi
git add -A
git commit -m "fix: resolve issue #${ISSUE_NUMBER}"
git push -u origin "$BRANCH"
PR_URL="$(gh pr create \
--title "Fix #${ISSUE_NUMBER}: ${ISSUE_TITLE}" \
--body "Closes #${ISSUE_NUMBER}" \
--head "$BRANCH" \
--base "${DEFAULT_BRANCH:-main}")"
gh issue comment "$ISSUE_NUMBER" --body "Agent opened ${PR_URL}"
The payload crosses into the runner through environment variables, never as shell input; ISSUE_TITLE and ISSUE_BODY are referenced, not executed. The npm test line is the placeholder for your own gate - pytest, cargo test, npm run verify - whatever your repo treats as green.
Runnable check: run the script once by hand against a dummy issue in a test repo: REPO=you/test ISSUE_NUMBER=1 ISSUE_TITLE="Add a README badge" ISSUE_BODY="..." GITHUB_TOKEN=... ./agent.sh. Watch it clone, run, and push. A runner you have not watched succeed once is not ready to run unattended.
The failure mode of unattended agents is not one bad run, it is bad runs at scale. From production experience, the rails that matter:
agent label is your opt-in per issue. No label, no run.ls-remote check makes a second delivery a quiet no-op instead of a conflicting push.timeout on the agent. A loop at 2am dies at the bound you set; 900 seconds fits comfortably inside Railway's model for background work.What you have now: a runner that is safe to point at real traffic, by construction.
Three files become a service. The Dockerfile is short because everything is a package: OpenCode installs from npm as opencode-ai, and gh and git come from apt.
FROM node:22-slim
RUN apt-get update && apt-get install -y git gh && rm -rf /var/lib/apt/lists/*
RUN npm install -g opencode-ai
WORKDIR /app
COPY server.js agent.sh ./
RUN chmod +x agent.sh
CMD ["node", "server.js"]
Push server.js, agent.sh, and the Dockerfile to a new GitHub repo. In Railway, create a new project and deploy from that repo - Railway detects the Dockerfile automatically. Then, in the service's Variables tab, add:
| Variable | Value |
|---|---|
GITHUB_WEBHOOK_SECRET | a long random string (you will reuse it in Step 6) |
GITHUB_TOKEN | the fine-grained token from Step 4 |
AGENT_MODEL | optional; defaults to opencode/deepseek-v4-flash |
TRIGGER_LABEL | optional; defaults to agent |
The Variables tab has a RAW editor for pasting a whole .env, and values can be sealed so they are never visible again. The agent inside the container reads the same env vars OpenCode reads locally, so no interactive login is ever needed on the server.
Last, expose it: Settings → Networking → Public Networking → Generate Domain. Railway provisions a *.railway.app domain with automatic SSL, which also satisfies GitHub's requirement to deliver webhooks over HTTPS. Service logs land in the dashboard, and a health check pointed at /healthz gets you uptime monitoring for free.
Runnable check: curl https://<your-service>.up.railway.app/healthz returns ok. Verify the deploy took the Dockerfile path (not the default buildpack) in the deployment logs.
In your target repository: Settings → Webhooks → Add webhook. The settings that matter, straight from the GitHub docs:
https://<your-service>.up.railway.app/webhookapplication/json (the JSON payload arrives as the raw request body)IssuesSave it. GitHub immediately sends a ping event, which is the first end-to-end proof that the endpoint is reachable, signed, and verified - the Recent Deliveries tab shows both sides, and your service logs show the 202.
Runnable check: the delivery shows a 200 or 202 response and your log line, and no run is spawned (a ping has no issue).
Open a real issue on the repo - a small, well-scoped task you know how to do, like "the README says port 4000 but the server listens on 3000" - and add the agent label before or right after creating it.
Then watch: the webhook fires, the signature checks, the receiver answers 202 in milliseconds, a clone happens, OpenCode reads the issue and edits code, the test gate runs, a branch lands, and a PR opens titled Fix #N: ... with "Closes #N" in the body. Review it like any other PR. If the diff is wrong, close it, tighten the issue text, and reopen - the loop is built for iteration.
What you have now: a repo where a labeled issue reliably becomes a reviewable pull request. The same receiver and runner generalize to other events with small changes - issue_comment for "do this follow-up", push to main for "post a release summary", or any HTTP client for internal tooling. The shape that matters is constant: verify, acknowledge fast, run one bounded job in a clean checkout, ship the result as a reviewable artifact. The harnesses post says it plainly - the script around the agent is the product. It also pays for itself: on Railway's Hobby plan, a service this small usually stays inside the included usage, and a bounded issue run costs cents in tokens.
Two gates: the TRIGGER_LABEL check means only issues explicitly tagged agent spawn a run, and the signature check means only GitHub deliveries are accepted at all. Everything else gets a 202 and a no-op.
Yes. The runner is language-agnostic: it clones any repo the token can read, and the npm test line is a placeholder for your repo's own gate (pytest, cargo test, npm run verify). The Dockerfile needs no change.
Railway's edge closes requests after 5 minutes with no data transferred, and GitHub terminates webhook deliveries that do not answer within 10 seconds. The build handles both: the receiver answers in milliseconds, and the agent runs as a background process afterward, well inside the 15-minute bound set by timeout 900. If your tasks genuinely need longer, move the runner to a queue-based worker.
Safe enough to let it open PRs, never to merge them. The label is the per-issue opt-in, the token is scoped to one repo, the fresh clone contains the blast radius, and the test gate runs before the branch is pushed. Keep branch protection on; your only job is reviewing.
On Railway's Hobby plan ($5/month with $5 of included usage), a service this small typically stays inside the included amount. Token costs are cents per issue with a budget model like DeepSeek V4 Flash at $0.14/$0.28 per million tokens. The real cost risk is unbounded loops, which the timeout and spending alerts handle.
| Source | URL |
|---|---|
| OpenCode CLI docs | https://opencode.ai/docs/cli/ |
| OpenCode GitHub | https://github.com/anomalyco/opencode |
| GitHub webhook events and payloads | https://docs.github.com/en/webhooks/webhook-events-and-payloads |
| GitHub webhook best practices | https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks |
| Validating webhook deliveries | https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries |
| GitHub REST: create a pull request | https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request |
| Railway Services | https://docs.railway.com/reference/services |
| Railway Public Networking | https://docs.railway.com/reference/public-networking |
| Railway Variables | https://docs.railway.com/develop/variables |
| Railway Pricing | https://docs.railway.com/reference/pricing |
Some links to tools above are referral links - see our affiliate disclosure.
Last updated: August 5, 2026
Read next
An agent CLI plus a cron schedule turns recurring dev chores into background work: dependency bumps, doc freshness checks, morning briefs. The pattern, the guardrails, and where to run it - your own hardware or a cloud host.
11 min readOpenCode is the fastest-growing open-source AI coding agent - 160K GitHub stars, 7.5M monthly users, 75+ model providers. Here is how to set it up, configure models, and use it effectively in your workflow.
11 min readDeepSeek shipped the official V4 Flash release on July 31, 2026. The re-post-trained 0731 build beats V4-Pro-Preview on agent benchmarks at $0.14/$0.28 per million tokens. Here is what changed and how to run it through OpenCode today.
7 min readTechnical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.
OpenAI's coding agent for terminal, cloud, IDE, GitHub, Slack, and Linear workflows. Reads repos, edits files, runs comm...
View ToolGoogle's asynchronous coding agent. Point it at a GitHub repo, it clones to a cloud VM, plans with Gemini, and opens a p...
View ToolWorkflow automation platform with native AI agent building. Visual editor plus JavaScript/Python code nodes, 500+ integr...
View ToolThe original AI coding assistant. 77M+ developers. Inline completions in VS Code and JetBrains. Copilot Workspace genera...
View ToolSpec out AI agents, run them overnight, wake up to a verified GitHub repo.
View AppTurn a one-liner into a working Claude Code skill. From idea to installed in a minute.
View AppTurn any GitHub repo into a shareable PNG. README hero in one shot.
View AppConfigure Claude Code for maximum productivity -- CLAUDE.md, sub-agents, MCP servers, and autonomous workflows.
AI AgentsWhat MCP servers are, how they work, and how to build your own in 5 minutes.
AI AgentsInstall Ollama and LM Studio, pull your first model, and run AI locally for coding, chat, and automation - with zero cloud dependency.
Getting Started
Open Design: Open-Source n8n App That Turns Any Website into a Brand Kit, Design System, HTML + Images The video introduces Open Design, an MIT-licensed full-stack template that combines AI and n8n a...

Learn The Fundamentals Of Becoming An AI Engineer On Scrimba; https://v2.scrimba.com/the-ai-engineer-path-c02v?via=developersdigest In today's video, I discuss Google's latest announcement...

No-Code AI Automation with VectorShift: Integrations, Pipelines, and Chatbots In this video, I introduce VectorShift, a no-code AI automation platform that enables you to create AI solutions...

An agent CLI plus a cron schedule turns recurring dev chores into background work: dependency bumps, doc freshness check...

OpenCode is the fastest-growing open-source AI coding agent - 160K GitHub stars, 7.5M monthly users, 75+ model providers...

DeepSeek shipped the official V4 Flash release on July 31, 2026. The re-post-trained 0731 build beats V4-Pro-Preview on...

A long-running coding agent is only useful if the environment around it can queue tasks, capture logs, checkpoint state,...

Five managed-agent providers, five pricing models, zero unified cost attribution. If you're running agents overnight, yo...

The agent is only as good as the prompt, and the best prompts are the ones you would speak. How to dictate context-rich...

New tutorials, open-source projects, and deep dives on coding agents - delivered weekly.