How Integration Webhooks (Slack, Linear, GitHub) Trigger Session Creation in Background Agents
Integration webhooks trigger session creation in Background Agents by validating incoming requests from external services, extracting user and repository context, and forwarding standardized create-session requests to the Control Plane, which spawns a Modal sandbox and returns a durable session ID.
The ColeMurray/background-agents repository implements three distinct bot services—Slack-Bot, Linear-Bot, and GitHub-Bot—that transform external events into active coding-agent sessions. Each service follows an identical pipeline: receive HTTP request, verify signatures, parse canonical fields, and invoke the Control Plane's session runtime. This unified architecture ensures that whether a developer comments in Slack, updates a Linear issue, or opens a GitHub pull request, the system provisions a sandboxed environment within seconds.
The Unified Webhook-to-Session Pipeline
All three integrations execute the same five-step flow to move from external event to active session:
- Webhook endpoint receives the HTTP request – Each bot exposes a dedicated route under its own package:
/eventsfor Slack,/webhookfor Linear, and/githubfor GitHub. - Signature and token verification ensures the request originates from the external service and has not been tampered with.
- Payload parsing extracts canonical fields required by
CreateSessionRequestas defined inpackages/shared/src/types/session-api.ts. - Control Plane client invocation constructs a request body matching
createSessionRequestSchemaand calls eithercreateSessionorcreateSessionRuntimeClientfrom the respectivepackages/*/src/sessions/control-plane-client.tsfiles. - Session instantiation –
createSessionRuntimeClientinpackages/control-plane/src/session/runtime-client.tscontacts the durable-object Session DO, persists a row in the D1 session index table, spawns a Modal sandbox, and returns the session ID to the caller.
The resulting pipeline flow is:
External Service → Bot Webhook Route → Verify & Parse → Control Plane Client → Session DO + Modal Sandbox → Session ID Returned
Slack Webhook Implementation
The Slack bot listens for interaction events at packages/slack-bot/src/routes/events.ts. Upon receiving a payload, it validates the Slack signature using headers x-slack-signature and x-slack-request-timestamp, then dispatches to the event handler:
// packages/slack-bot/src/routes/events.ts
eventRoutes.post("/events", async (c) => {
const signature = c.req.header("x-slack-signature") ?? null;
const timestamp = c.req.header("x-slack-request-timestamp") ?? null;
const rawPayload = await c.req.text();
// Verify Slack signature …
const parsedPayload = slackInteractionPayloadSchema.safeParse(rawPayload);
if (!parsedPayload.success) throw new Error("Invalid payload");
// Dispatch to the Slack event handler which eventually calls createSession()
await handleSlackEvent(env, parsedPayload.data);
});
The event handler delegates to launchSession in packages/slack-bot/src/sessions/session-launcher.ts, which imports the Control Plane client and constructs the session request:
// packages/slack-bot/src/sessions/session-launcher.ts
import { createSession } from "./control-plane-client";
export async function launchSession(userId: string, repoInfo: RepoInfo) {
const session = await createSession(env, {
source: "slack",
actorUserId: userId,
repositories: [{ repo: repoInfo.repo, branch: repoInfo.branch }],
});
return session.sessionId;
}
Linear Webhook Implementation
The Linear bot handles incoming webhooks at packages/linear-bot/src/webhook-handler.ts. After validating the Linear signature, it maps the issue and branch information to the standardized request shape:
// packages/linear-bot/src/webhook-handler.ts
export async function webhookHandler(req: Request) {
const body = await req.json();
// Validate Linear signature (omitted for brevity)
// Linear provides issue/branch info – map to CreateSessionRequest shape
const payload = {
source: "linear",
actorUserId: body.actor.id,
repositories: [{ repo: body.repository.id, branch: body.branch.name }],
};
// Direct call to the same control‑plane helper
const result = await createSession(env, payload);
return new Response(JSON.stringify({ sessionId: result.sessionId }));
}
This handler directly constructs the payload and invokes createSession, bypassing intermediate dispatchers while maintaining the same schema contract.
GitHub Webhook Implementation
The GitHub integration resides in the Control Plane package at packages/control-plane/src/webhooks/github.ts. It uses the runtime client factory to instantiate a session manager:
// packages/control-plane/src/webhooks/github.ts
import { createSessionRuntimeClient } from "../session/runtime-client";
export async function handleGithubWebhook(env, ctx, request) {
const body = await request.json();
// Verify GitHub signature …
const sessionRuntime = createSessionRuntimeClient(env, ctx);
const session = await sessionRuntime.createSession({
source: "github",
actorUserId: body.sender.login,
repositories: [{ repo: body.repository.full_name, branch: body.pull_request.head.ref }],
});
return new Response(JSON.stringify({ sessionId: session.id }));
}
In this implementation, createSessionRuntimeClient returns an object exposing the createSession method, which ultimately persists the session state and provisions the Modal sandbox.
Shared Session Schema
All three bots serialize their extracted context into a uniform payload defined by Zod in the shared types package:
// packages/shared/src/types/session-api.ts
export const createSessionRequestSchema = z.object({
source: z.enum(["slack", "linear", "github"]),
actorUserId: z.string(),
repositories: z.array(
z.object({ repo: z.string(), branch: z.string() })
),
// …optional fields such as environmentId, tags, etc.
});
This schema ensures type safety across the Slack, Linear, and GitHub implementations while allowing the Control Plane to handle session creation polymorphically regardless of the originating service.
Summary
- Three bot services (Slack, Linear, GitHub) listen for external events on dedicated routes and verify incoming signatures before processing.
- Canonical field extraction maps disparate webhook payloads to the uniform
createSessionRequestSchemadefined inpackages/shared/src/types/session-api.ts. - Control Plane abstraction – Calls to
createSessionorcreateSessionRuntimeClientdelegate session instantiation topackages/control-plane/src/session/runtime-client.ts. - Session creation persists state in the D1 Session DO and spawns a Modal sandbox, returning a durable session ID to the webhook caller.
- Unified architecture allows ColeMurray/background-agents to treat Slack messages, Linear issues, and GitHub pull requests as equivalent triggers for background agent execution.
Frequently Asked Questions
How does Background Agents verify that webhooks actually come from Slack, Linear, or GitHub?
Each bot implements signature verification specific to the external service. The Slack bot checks x-slack-signature and x-slack-request-timestamp headers in packages/slack-bot/src/routes/events.ts. The Linear and GitHub bots perform comparable HMAC signature validation against shared secrets before parsing the JSON payload, ensuring that only genuine service requests trigger session creation.
What happens if the createSession call fails after the webhook is received?
If the Control Plane client throws an error during session creation, the bot's route handler will typically return an HTTP error response (500 or 502) to the external service. Because the webhook has already been acknowledged, the external service (Slack, Linear, or GitHub) may retry delivery depending on its own retry policies, though the specific error handling logic depends on the individual bot's implementation in its respective webhook-handler.ts or route file.
Can I add custom metadata to the session when triggering from a webhook?
Yes. The createSessionRequestSchema in packages/shared/src/types/session-api.ts includes optional fields such as environmentId and tags. When constructing the payload in the bot's session launcher or webhook handler, you can populate these fields to attach metadata that will be persisted alongside the session in the D1 index and available to the running agent in the Modal sandbox.
Why does the GitHub implementation use createSessionRuntimeClient while Slack uses createSession?
createSessionRuntimeClient is a factory function that returns a session manager instance bound to the current environment and execution context, providing additional methods beyond simple creation. The Slack and Linear bots import a simpler createSession helper that wraps this factory for convenience. Both ultimately invoke the same underlying logic in packages/control-plane/src/session/runtime-client.ts to instantiate the Session DO and spawn the sandbox.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →