← Back to Cookbook

PR Review Notifier

Receives GitHub PR webhooks, summarizes changes with AI, and posts to Slack.

aihttpswitch

Source

/**
 * Receives GitHub PR webhook events, summarizes changes with AI,
 * and posts a review summary to Slack.
 */

secret pr_config {
  vars: [SLACK_WEBHOOK_URL]
}

webhook github_pr {
  label: "GitHub PR Event"
  schema: @json {
    {
      "type": "object",
      "required": ["action", "pull_request"],
      "properties": {
        "action": { "type": "string" },
        "pull_request": {
          "type": "object",
          "properties": {
            "title": { "type": "string" },
            "body": { "type": "string" },
            "user": { "type": "object" },
            "html_url": { "type": "string" },
            "additions": { "type": "number" },
            "deletions": { "type": "number" },
            "changed_files": { "type": "number" }
          }
        }
      }
    }
  }
}

graph notify_pr {
  label: "Notify PR"

  root {
    type: code
    label: "Extract PR data"
    code: @ts {
      const input = context.nodes.root.input
      const pr = input.pull_request || {}
      return {
        action: input.action,
        title: pr.title || "Untitled",
        body: pr.body || "",
        author: pr.user ? pr.user.login : "unknown",
        url: pr.html_url || "",
        additions: pr.additions || 0,
        deletions: pr.deletions || 0,
        changed_files: pr.changed_files || 0
      }
    }
    outputSchema: @json {
      {
        "type": "object",
        "properties": {
          "action": { "type": "string" },
          "title": { "type": "string" },
          "body": { "type": "string" },
          "author": { "type": "string" },
          "url": { "type": "string" },
          "additions": { "type": "number" },
          "deletions": { "type": "number" },
          "changed_files": { "type": "number" }
        }
      }
    }
  }

  node route_action {
    type: switch
    label: "Filter action"
    cases: ["opened", "ignore"]
    router: @ts {
      const action = context.nodes.root.output.action
      if (action === "opened" || action === "ready_for_review") return "opened"
      return "ignore"
    }
  }

  node summarize {
    type: ai
    label: "Summarize PR"
    kind: text
    model: "google/gemini-2.5-flash"
    prompt: @ts {
      const pr = context.nodes.root.output
      return "Write a 1-2 sentence summary of this pull request for a Slack notification.\n\nTitle: " + pr.title + "\nDescription: " + pr.body + "\nFiles changed: " + pr.changed_files + " (+" + pr.additions + " -" + pr.deletions + ")"
    }
    maxTokens: 150
  }

  node post_slack {
    type: http
    label: "Post to Slack"
    method: "POST"
    url: @ts { return context.secrets.pr_config.SLACK_WEBHOOK_URL }
    body: @ts {
      const pr = context.nodes.root.output
      const summary = context.nodes.summarize.output
      return JSON.stringify({
        text: "*New PR:* " + pr.title + " by " + pr.author + "\n" + summary + "\n<" + pr.url + "|View PR>"
      })
    }
    secrets: {
      pr_config: [SLACK_WEBHOOK_URL]
    }
  }

  node skip {
    type: code
    label: "Skip"
    code: @ts {
      return { skipped: true, action: context.nodes.root.output.action }
    }
  }

  flow {
    root -> route_action
    route_action -["opened"]-> summarize
    summarize -> post_slack
    route_action -["ignore"]-> skip
  }
}

trigger on_pr {
  webhook:github_pr -> notify_pr
  enabled: true
}

Flow

Trigger → graph

Graph nodes