Provider connections

Four providers have a first-class trigger: GitHub, Linear, Jira and Notion. A connection is org-level: you connect the provider once from Connectionsin the dashboard, through the vendor's own consent screen, and the platform verifies each delivery, dedupes it, filters it against your declared triggers, and only then creates a run. The providers differ in the events they carry, in what each vendor makes Boardwalk do to subscribe, and in what credential (if any) Boardwalk keeps:

ProviderWhat Boardwalk storesWhy
GitHubNo installation token, no private keyNo provider credential at all.
LinearThe grant, in your org's vaultRemoving the webhook at disconnect needs it once more. Never vended to a run.
JiraThe Atlassian grant, in your org's vaultRenewal and disconnect need it. Never vended to a run.
NotionNothing: the token is droppedOnly the workspace identity is read from the token response.

How provider triggers behave

Everything in this section holds for all four:

  • No URL, no secret, no signature code. The connection replaces all of it.
  • Semantic events only. event names an outcome (pr.merged, issue.status_changed), not a raw provider action, so you never re-derive a vendor's conditions in your program. There is no raw-event escape hatch: if the vocabulary doesn't carry what you need, use a plain webhook trigger and point the provider at it.
  • A trigger deploys before its connection exists. It shows a needs connectionstate with a Connect link rather than failing the deploy, so a package stays portable between orgs. It simply doesn't fire until the connection covers it.
  • Every delivery is logged and replayable in the inbound delivery log, including why one produced no run. See Runs & observability.
  • One provider account can drive several orgs. Each org claims it through its own consent and proves access independently, and a delivery fans out to every active claim.
  • The run input is a curated projection of the vendor payload, not the raw object, and it is stable across vendor API churn. Inside the run, context.trigger.kind is webhook and context.trigger.source is <provider>:<event>. Branch on input.event rather than the trigger kind.
  • Issue titles, descriptions and comments are untrusted text written by other people. Treat them as data for an agent to describe, not instructions to follow, and keep tools and secrets on the code side. See Quarantine untrusted input.

Because the vocabularies are deliberately parallel, one workflow can serve two trackers: declare a linear and a jira trigger on the same program, and input.event reads issue.created either way.

GitHub

workflow.jsonc
"triggers": [
  { "kind": "github", "event": "pr.merged", "repos": ["acme/app"] }
]

Fire on a GitHub event, delivered through your org's Boardwalk GitHub connection. repos narrows to specific repositories by owner/name; omit it and the trigger covers every repo the installation can see. It is the one provider whose trigger takes a scope filter.

EventFires when
pr.openedA pull request is opened, or a draft is marked ready for review. Never fires for a PR whose head is a fork.
pr.mergedA pull request is closed and merged. This one does fire for merged fork PRs.
issue.openedAn issue is opened.
issue.commentedA comment lands on a real issue. Pull request conversation threads are excluded.
ci.completedA check suite completes, which covers GitHub Actions and any other provider that reports through the Checks API.

The fork rule on pr.openedis built in, not a setting: an outside contributor's PR must not start an agent holding your repo's credentials. pr.mergedis deliberately different, because a merge is the maintainer's own act and the code is in your base branch either way. A draft PR fires nothing when opened; it fires pr.opened when it is marked ready.

Install the Boardwalk GitHub App once per org from Connectionsin the dashboard, choosing which repositories it can see. A trigger naming a repo the installation doesn't cover stays quiet, and the delivery log says so. Boardwalk stores no installation token and holds no private key for your repositories.

Every event carries event, repo, sender and deliveryId; the event family adds exactly one of pr, issue (with comment on issue.commented), or checkSuite:

src/index.ts
interface GithubEvent {
  event: "pr.opened" | "pr.merged" | "issue.opened" | "issue.commented" | "ci.completed";
  repo: { fullName: string; name: string; owner: string; private: boolean; defaultBranch: string | null };
  sender: { login: string; type: string | null } | null;
  deliveryId: string;
  pr?: { number: number; title: string; body: string | null; url: string; merged: boolean;
         authorLogin: string; labels: string[]; baseRef: string; headRef: string; headSha: string };
  issue?: { number: number; title: string; body: string | null; url: string; labels: string[] };
  comment?: { id: number; body: string; url: string; authorLogin: string };
  checkSuite?: { id: number; conclusion: string | null; headBranch: string | null; headSha: string };
}

export default async function run(input: GithubEvent): Promise<string> {
  return agent(`Write release notes for merged PR #${input.pr?.number}: ${input.pr?.title}`);
}

Linear

workflow.jsonc
"triggers": [
  { "kind": "linear", "event": "issue.status_changed" }
]
EventFires when
issue.createdAn issue is created.
issue.status_changedAn issue moves to a different workflow state. Other edits (title, assignee, labels) fire nothing.
issue.commentedA comment is created on an issue.

Connecting sends you to Linear's consent screen, after which Boardwalk creates the workspace webhook itself, subscribed to issues and comments across all public teams and signed with a secret it mints. The workspace identity comes from the token Linear returns rather than from the redirect, so a workspace can only route to the org that consented. Use Manage to connect another workspace; disconnecting removes the webhooks Boardwalk created.

Unlike the GitHub connection, which holds no token at all, the Linear grant is kept in your org's vault: removing the webhook at disconnect needs it once more. It is never vended to a run. Anything your program does in Linear uses your own credential through secrets or an MCP connection.

src/index.ts
interface LinearEvent {
  event: "issue.created" | "issue.status_changed" | "issue.commented";
  organizationId: string;
  deliveryId: string;
  issue?: { id: string; identifier: string | null; title: string | null; url: string | null;
            teamKey: string | null; stateName: string | null; priority: number | null;
            assignee: string | null; labels: string[]; description: string | null };
  comment?: { id: string; body: string | null; url: string | null; issueId: string | null;
              issueIdentifier: string | null; issueTitle: string | null;
              authorName: string | null };
  previousStateId?: string | null; // on issue.status_changed, when Linear sent the prior state
}

export default async function run(input: LinearEvent): Promise<string> {
  return agent(`Summarize ${input.issue?.identifier}: ${input.issue?.title}`);
}

issue is set for the issue events, comment for issue.commented (carrying the identifier and title of the issue it landed on). Fields are individually nullable because the serialized entity varies with workspace configuration.

Jira

workflow.jsonc
"triggers": [
  { "kind": "jira", "event": "issue.created" }
]
EventFires when
issue.createdAn issue is created.
issue.status_changedAn issue transitions status, meaning an update whose changelog carries a status item. Summary edits, assignment and label churn fire nothing.
issue.commentedA comment is created on an issue.

One Atlassian consent can cover several Jira sites, and each site becomes its own connection. Boardwalk registers the site webhooks for you, scoped to the projects that site has at connect time. Jira offers no match-all scope, so a project created later is not covered until you re-register with Manage. Site webhooks expire after 30 days; a daily sweep renews them ahead of time, and when a renewal fails the connection shows as needing a reconnect rather than going quiet. The Atlassian grant lives in your org's vault because renewal and disconnect need it, and is never vended to a run.

src/index.ts
interface JiraEvent {
  event: "issue.created" | "issue.status_changed" | "issue.commented";
  deliveryId: string;
  issue?: { id: string; key: string | null; summary: string | null; url: string | null;
            projectKey: string | null; statusName: string | null; issueType: string | null;
            priority: string | null; assigneeAccountId: string | null;
            labels: string[]; description: string | null };
  comment?: { id: string; body: string | null; authorAccountId: string | null };
  statusChange?: { from: string | null; to: string | null }; // on issue.status_changed
  actorAccountId?: string | null;                            // who caused the event
}

export default async function run(input: JiraEvent): Promise<string> {
  return agent(`Triage ${input.issue?.key}: ${input.issue?.summary}`);
}

Two things to know about the projection. People are bare account ids, never names or email addresses: Boardwalk stores no Atlassian profile data, so resolve an id to a person with your own credential when you need to. And description and comment body are plain text only. Sites on the newer API send rich documents there, which project as null, so fetch the issue when you need its full body.

Notion

workflow.jsonc
"triggers": [
  { "kind": "notion", "event": "page.updated" }
]
EventFires when
page.createdA page is created.
page.updatedA page's content or its properties change. Notion aggregates rapid edits, so a burst of typing is one delivery, not one per keystroke.
comment.createdA comment is created.

Connecting sends you to Notion's consent screen, where you choose which pages the integration can see. Boardwalk reads the workspace identity from the token response and then drops the token: no Notion credential is stored at all. Deliveries arrive on Boardwalk's integration-level endpoint and are routed to your org by workspace.

src/index.ts
interface NotionEvent {
  event: "page.created" | "page.updated" | "comment.created";
  deliveryId: string;
  workspaceName: string | null;
  entity: { id: string; type: string | null };          // the page or comment involved
  parent: { id: string; type: string | null } | null;   // its database, page, or workspace
  authorIds: string[];                                  // bare Notion user or bot ids
}

export default async function run(input: NotionEvent): Promise<string> {
  const page = await fetchPage(input.entity.id); // your credential, your call
  return agent(`Summarize this page: ${page.text}`);
}

A Notion payload carries ids, not content. The trigger is a change signal, so the program fetches what it needs with your own credential through secrets or an MCP connection. Authors are bare ids for the same reason Jira's are, so no names or email addresses pass through the platform.