ChatGPT can do more than spit out text—it can call your code. By coupling its function‑calling feature with AWS EventBridge Pipes you turn a chat response into a fully‑managed, serverless pipeline that runs instantly. This post shows exactly how to wire the two together so every PR gets an AI‑driven review without leaving your repo.
Understanding ChatGPT Function Calls
Why it matters
When you ask ChatGPT “review this diff”, the model could just give you a paragraph of text. A function call lets the model hand you a structured payload instead—think of it as a robot reaching into its toolbox and handing you a pre‑shaped screwdriver rather than a vague suggestion. That payload can be sent directly to another system (e.g., a Lambda) without any manual copy‑paste.
The pieces
-
Function‑calling API – a special endpoint (
/v1/chat/completions) where you describe a function schema (name, description, JSON parameters). The model replies with afunction_callobject if it decides the function fits the conversation. -
Arguments – the JSON object that satisfies the schema. Important: EventBridge only sees a string, so you must
JSON.stringifythe arguments before you publish them. If you forget, the event disappears silently.
In plain English: The model doesn’t send a raw JavaScript object; it sends a string that looks like JSON. Treat it like a sealed envelope—you have to open (parse) it on the other side.
Minimal example of a function schema
{
"name": "requestCodeReview",
"description": "Ask the AI to produce a markdown code review for a git diff",
"parameters": {
"type": "object",
"properties": {
"diff": { "type": "string", "description": "Unified diff of the PR" },
"prNumber": { "type": "integer", "description": "Pull request identifier" }
},
"required": ["diff"]
}
}
When the model decides to call requestCodeReview, its reply contains:
{
"name": "requestCodeReview",
"arguments": "{\"diff\":\"--- a/file.ts\\n+++ b/file.ts\\n@@ -1,3 +1,5 @@...\",\"prNumber\":42}"
}
Notice the arguments field is already a JSON string—that is the format you must forward to EventBridge.
Introducing EventBridge Pipes
Why it matters
EventBridge Pipes is a low‑code connector that moves events from a source (e.g., an API Gateway, an SQS queue, or a custom HTTP endpoint) to a target (e.g., a Lambda, Step Functions, or another EventBridge bus). Think of a pipe as a literal water pipe: water (events) flows from the faucet (source) to the sink (target) without you having to build a pump or valve each time.
Core concepts
- Source – where the event originates. In our case the source will be an HTTP webhook that receives the function‑call payload from OpenAI.
- Target – what processes the event. Here it is a TypeScript Lambda that talks to OpenAI again (to get the review) and stores the result in S3.
- Filters – optional rules that decide whether an event should continue down the pipe. Filters run in under 5 seconds; if the logic is too heavy the pipe simply drops the event.
Tip: Keep filter expressions short and simple (e.g.,
detail.type = 'code_review'). Complex JavaScript in a filter will hit the 5‑second limit and fail silently.
Known quirks to keep in mind
| Issue | What happens | How to avoid |
|---|---|---|
| 5‑second filter limit | Pipe discards events without error | Use only attribute‑based matches, move heavy logic into the Lambda |
| Scheduler timezone & DST | Scheduled pipes may run an hour early/late during daylight‑saving switches | Explicitly set ScheduleExpressionTimezone to UTC and adjust logic inside the Lambda |
| Schema Registry cold‑start | EventBridge won’t infer a schema until the first event flows, causing a delay | Deploy a dummy event after creating the pipe to “warm‑up” the registry |
| Cross‑account policies | Missing resource‑based policy blocks delivery | Add a policy allowing events.amazonaws.com from the source account to put events into the target account |
| High load delivery delay | Under heavy traffic you can see 30 + seconds latency | Enable deduplication and consider a dedicated DLQ to monitor back‑pressure |
Building the Lambda Consumer
Why it matters
The Lambda is the brain that turns a raw diff into a polished markdown review and then saves it. By keeping the code small and well‑commented, even a developer new to AWS can understand the flow.
Prerequisites
- Node 22 runtime (the latest LTS as of 2026).
- Packages:
@aws-sdk/client-s3for S3 writes,node-fetch(or nativefetch) for the OpenAI call. - Remember the SnapStart + VPC gotcha: if you attach the Lambda to a VPC you lose the cold‑start benefit of SnapStart, because the network interface attachment dominates the startup time.
Full Lambda code
// src/handler.ts
import {
S3Client,
PutObjectCommand,
} from "@aws-sdk/client-s3"; // SDK for writing to S3
import { APIGatewayProxyEventV2 } from "aws-lambda"; // Type for the incoming event
import fetch from "node-fetch"; // HTTP client for OpenAI
// ---------------------------------------------------------------------------
// Configuration – keep these values in environment variables in production
// ---------------------------------------------------------------------------
const OPENAI_API_KEY = process.env.OPENAI_API_KEY!;
const S3_BUCKET = process.env.S3_BUCKET!; // bucket where reviews land
const REVIEW_PREFIX = "code-reviews/"; // folder inside the bucket
// Re‑use SDK clients across invocations – saves time
const s3 = new S3Client({});
// ---------------------------------------------------------------------------
// Helper: call OpenAI with the function schema and the diff
// ---------------------------------------------------------------------------
async function askForReview(diff: string, prNumber: number): Promise<string> {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-mini", // a fast, cheap model for code review
messages: [
{ role: "system", content: "You are a helpful code reviewer." },
{ role: "user", content: `Review this diff for PR #${prNumber}:\n${diff}` },
],
// Tell the model we expect a structured function call
functions: [
{
name: "requestCodeReview",
description: "Generate a markdown formatted code review",
parameters: {
type: "object",
properties: {
review: {
type: "string",
description: "Markdown with the review comments",
},
},
required: ["review"],
},
},
],
// Force the model to actually call the function
function_call: { name: "requestCodeReview" },
}),
});
const data = await response.json();
// The model returns a `function_call` object inside `choices[0].message`
const fnCall = data.choices?.[0]?.message?.function_call;
if (!fnCall) {
throw new Error("OpenAI did not return a function call");
}
// `arguments` is a JSON string – parse it to get the markdown
const args = JSON.parse(fnCall.arguments);
return args.review; // the markdown we need
}
// ---------------------------------------------------------------------------
// Lambda entry point – receives an EventBridge Pipe event
// ---------------------------------------------------------------------------
export const handler = async (event: any) => {
// EventBridge pipes wrap the original source event under `detail`
const payload = event.detail;
// The payload should contain a stringified JSON object from ChatGPT
const functionCall = JSON.parse(payload);
// Guard against missing fields – a common source of silent failures
if (!functionCall.arguments) {
console.error("Missing arguments field – EventBridge may have dropped the payload");
return;
}
// Parse the arguments that were stringified by ChatGPT
const args = JSON.parse(functionCall.arguments);
const diff = args.diff;
const prNumber = args.prNumber ?? 0;
// Call OpenAI to get the markdown review
const markdown = await askForReview(diff, prNumber);
// Build the S3 key: e.g., code-reviews/pr-42.md
const s3Key = `${REVIEW_PREFIX}pr-${prNumber}.md`;
// Store the markdown in S3
await s3.send(
new PutObjectCommand({
Bucket: S3_BUCKET,
Key: s3Key,
Body: markdown,
ContentType: "text/markdown",
})
);
console.log(`Review for PR #${prNumber} saved to s3://${S3_BUCKET}/${s3Key}`);
};
Explanation of each block
-
Imports – bring in the S3 client (
@aws-sdk/client-s3) andfetchfor the OpenAI HTTP call. - Configuration – read secrets from environment variables; never hard‑code them.
-
askForReview– builds a chat request that tells the model to invokerequestCodeReview. The model’s response is a JSON string insidefunction_call.arguments. We parse it and return the markdown. -
handler– the entry point that EventBridge Pipes invokes. The pipe puts the original payload underdetail. We parse the stringified arguments, call OpenAI, then write the result to S3.
Key takeaway: The Lambda does three tiny jobs – decode the ChatGPT envelope, ask the model for a structured review, and persist the result. Keeping each step separate makes debugging easier, especially when EventBridge silently drops events.
Connecting ChatGPT to EventBridge via Function Calls
Why it matters
ChatGPT can’t push directly to EventBridge, but it can return a function‑call payload that your webhook can turn into an EventBridge event. That webhook is the glue that transforms the model’s response into a pipe‑ready message.
The webhook flow
- Your application (could be an API Gateway + Lambda, or a simple Express server) receives the HTTP response from OpenAI.
- It extracts
function_call.arguments, stringifies it (if it isn’t already), and publishes it to an EventBridge Pipe source (an Event Bus). - The pipe’s filter checks
detail.type = 'code_review'and forwards the event to the Lambda consumer we built earlier.
Minimal webhook code (Node 22, using @aws-sdk/client-eventbridge)
// src/webhook.ts
import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge";
import express, { Request, Response } from "express";
const app = express();
app.use(express.json()); // parse JSON bodies
const eb = new EventBridgeClient({}); // uses default credentials/region
// Endpoint that OpenAI calls back with the function payload
app.post("/openai-callback", async (req: Request, res: Response) => {
const fnCall = req.body?.function_call;
if (!fnCall) {
return res.status(400).json({ error: "Missing function_call" });
}
// IMPORTANT: EventBridge expects a **string** in the `Detail` field
const detailString = JSON.stringify(fnCall);
try {
await eb.send(
new PutEventsCommand({
Entries: [
{
EventBusName: process.env.EVENT_BUS!, // name of the custom bus
Source: "openai.chat", // arbitrary source identifier
DetailType: "code_review", // used by pipe filter
Detail: detailString, // the stringified payload
},
],
})
);
res.status(202).send(); // accepted
} catch (err) {
console.error("Failed to publish to EventBridge:", err);
res.status(500).json({ error: "EventBridge publish failed" });
}
});
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`Webhook listening on ${PORT}`));
What each line does
-
express.json()– makes Express parse the incoming JSON body. -
fnCall– the object OpenAI returned underfunction_call. -
JSON.stringify(fnCall)– converts the object to a string; without this the pipe silently discards the event (the gotcha we warned about). -
PutEventsCommand– the SDK call that publishes an event to a specific Event Bus.
Tip: Keep the
DetailTypeshort and consistent; the Pipe filter will reference it directly (detail-type = 'code_review').
Setting up the Pipe (AWS Console / CDK snippet)
import {
CfnPipe,
CfnPipeProps,
} from "aws-cdk-lib/aws-pipes";
import { Construct } from "constructs";
export class ReviewPipe extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const pipeProps: CfnPipeProps = {
roleArn: process.env.PIPE_ROLE_ARN, // role that allows EventBridge to invoke Lambda
source: `arn:aws:events:${process.env.AWS_REGION}:${process.env.AWS_ACCOUNT_ID}:event-bus/${process.env.EVENT_BUS}`,
target: `arn:aws:lambda:${process.env.AWS_REGION}:${process.env.AWS_ACCOUNT_ID}:function:${process.env.REVIEW_LAMBDA_NAME}`,
// Simple filter – only events whose DetailType equals "code_review" go through
filterCriteria: {
filters: [
{
pattern: JSON.stringify({
"detail-type": [{ "prefix": "code_review" }],
}),
},
],
},
};
new CfnPipe(this, "CodeReviewPipe", pipeProps);
}
}
Important notes
- The pipe’s role must have
events:PutEventson the source bus andlambda:InvokeFunctionon the target. - The filter pattern is evaluated in under 5 seconds; we only match on
detail-type, staying well inside the limit.
In plain English: The pipe is a tiny rule‑engine that says “if the event says it’s a code review, hand it to my Lambda”. Anything more complicated (e.g., regex on the diff) would exceed the evaluation window.
End‑to‑End Walkthrough: From PR to AI Review
The journey, step by step
- Developer opens a pull request – GitHub’s webhook posts the PR diff to a GitHub Action or a custom listener.
-
Your service sends the diff to OpenAI – using the chat‑completion API with the
requestCodeReviewfunction schema. -
OpenAI decides to call the function – returns a payload like
{ name: "requestCodeReview", arguments: "{\"diff\":\"...\"}" }. -
Your webhook (
/openai-callback) receives the payload – stringifies it and publishes to EventBridge. -
EventBridge Pipe evaluates the filter – sees
detail-type = "code_review"and forwards the event to the Lambda consumer. - Lambda parses the arguments, calls OpenAI again (the second call is where the model actually writes the markdown), and stores the result in S3.
- S3 notification (optional) – could trigger a comment on the PR with a link to the markdown file, completing the loop.
Visual analogy
Imagine a kitchen:
- ChatGPT is the chef who prepares a sauce but hands you a sealed jar (the function call).
- Your webhook is the kitchen assistant who reads the label, puts the jar on the conveyor belt (EventBridge).
- EventBridge Pipes is the moving walkway that only lets jars with a red sticker (our filter) continue.
- Lambda is the plating station that uncaps the jar, adds garnish (the markdown review), and puts the plate on the serving window (S3).
If any step drops the jar, the meal never reaches the table.
Debugging checklist
| Step | Common mistake | Symptom | Fix |
|---|---|---|---|
| OpenAI request | omitted function_call field |
Model returns plain text instead of structured call | Add "function_call": {"name":"requestCodeReview"}
|
| Webhook | sent raw object in Detail
|
EventBridge silently discards event |
JSON.stringify the whole function_call object |
| Pipe filter | used complex JMESPath expression | Event never reaches Lambda, no error logs | Reduce filter to attribute equality (detail-type) |
| Lambda | parsed detail as object directly |
undefined arguments, runtime error |
const payload = JSON.parse(event.detail); |
| S3 write | missing Content-Type header |
Review appears as binary gibberish in UI | Set ContentType: "text/markdown"
|
Key takeaway: The most frequent silent failure is forgetting to stringify the arguments before publishing to EventBridge. Treat that as a “must‑do” step, just like setting the correct IAM permissions.
The Takeaway
- Function calls turn ChatGPT from a static writer into an event producer – the model gives you a JSON string that can be routed elsewhere.
- EventBridge Pipes act as a lightweight, serverless router – they move the payload from your webhook to a Lambda without you managing queues or polling.
-
Stringify the
argumentsfield; otherwise the pipe drops the event without a warning. - Keep pipe filters tiny (attribute matches) to stay under the 5‑second evaluation limit.
- Separate concerns in the Lambda – decode, request, store. This makes debugging straightforward when something goes missing.
- Watch out for known AWS gotchas (SnapStart + VPC, cross‑account policies, Scheduler DST) early in the design to avoid surprise costs or latency spikes.
By wiring these pieces together you get an automated, real‑time code‑review loop that lives entirely in AWS’s managed services. No servers to patch, no cron jobs to maintain, and every pull request gets a fresh AI‑generated review the moment it lands. Happy coding!
Transparency notice
This article was written with the help of an AI system — Groq (GPT OSS 120B).
Published: 2026-09-21 · Primary focus: EventBridge
All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.Find an error? Drop a comment — corrections are always welcome.
Top comments (0)