Guide · AWS EventBridge Pipes
EventBridge Pipes Enrichment
Enrichment in EventBridge Pipes inserts a processing step between the source and the target: every batch of filtered records is passed to a Lambda function, API Gateway endpoint, or Step Functions Express Workflow, and the enrichment's response replaces the batch payload before it reaches the target. For MCP servers this is the canonical place to hydrate raw tool call events with tenant metadata, validate token budgets, or format payloads to match a downstream schema — without putting that logic in the source Lambda or the target. Three critical things teams get wrong: Lambda enrichment has a hard 29-second timeout enforced by Pipes, even if your Lambda function has a 15-minute timeout configured on the function itself — if enrichment takes longer than 29 seconds the pipe treats it as a failure and retries the batch. The enrichment response must cover all records in the batch — if you return fewer items than you received, the remaining records are silently dropped, not retried; the enrichment output length determines what the target receives. Enrichment failure is all-or-nothing for the batch — there is no per-record failure granularity (unlike Lambda ESM with batchItemFailures); if enrichment throws or returns a non-2xx, the entire batch fails and enters the retry/DLQ flow.
TL;DR
Enrichment Lambda gets the full batch as an array and must return an array of the same length (or an empty array to drop all records). Keep enrichment under 25 seconds to have margin before the 29-second Pipes cutoff. For API Gateway enrichment, map the batch to the request body and parse the response. For Step Functions, only Express Workflows are supported (synchronous execution). Enrichment failures retry the entire batch; use a pipe-level DLQ for unrecoverable failures.
Lambda enrichment — input shape and response contract
When Pipes invokes a Lambda enrichment function, it sends an array of source records as the event payload. The exact shape depends on the source type (SQS records, Kinesis records, or DynamoDB stream records). The enrichment Lambda must return an array — its response is what EventBridge Pipes passes to the target, completely replacing the original source records.
// Lambda enrichment for SQS-sourced pipe
// Event = array of SQS records (filtered by Pipes before reaching enrichment)
exports.handler = async (events: SQSRecord[]): Promise
Input and output transformation (InputTemplate / OutputTemplate)
Before Pipes passes source records to the enrichment function, and before it passes enrichment output to the target, you can apply a JSONPath-based input transformation. This is useful for unwrapping the SQS envelope (passing only $.body to enrichment) or reshaping the enrichment output to match a specific target schema.
// CDK: use inputTemplate to unwrap SQS body before enrichment
const pipe = new pipes.CfnPipe(this, "EnrichedPipe", {
// ...
enrichment: enrichFn.functionArn,
enrichmentParameters: {
// Pass only the parsed SQS message body to enrichment Lambda
// enrichment receives: { toolName: "search", tenantId: "t-99", ... }
// instead of the full SQS record envelope
inputTemplate: "$.body",
// Alternative: compose a custom object from multiple paths
// inputTemplate: JSON.stringify({
// "messageId": "<$.messageId>",
// "body": "<$.body>",
// "tenantId": "<$.messageAttributes.tenantId.stringValue>"
// }),
},
// Target input transformation — reshape enrichment output for DynamoDB
targetParameters: {
dynamoDbParameters: {
operation: "PUT_ITEM",
// input transformer can map enriched fields to DynamoDB attribute format
},
},
});
When using inputTemplate, template variables use angle-bracket JSONPath syntax: "<$.field.path>". A bare "$.body" (without angle brackets) extracts the entire body object as the sole event passed to enrichment. The angle-bracket syntax is used when embedding field values inside a JSON template string.
Enrichment timeout — the 29-second wall
EventBridge Pipes hard-codes a 29-second timeout for enrichment Lambda invocations. This is independent of the Lambda function's own configured timeout:
- If your Lambda function is configured with a 5-minute timeout, Pipes still waits only 29 seconds and then treats the invocation as a failure.
- The Lambda function may still continue executing after Pipes gives up — this can lead to partial work with no Pipes awareness. Use idempotent writes in enrichment (conditional DynamoDB puts, deduplication keys) to handle this.
- Pipes does not send a cancellation signal to the Lambda when it times out — the Lambda invocation continues until it finishes or until its own timeout, but its response is ignored by Pipes.
The practical implication: enrichment must complete in under 25 seconds to have a safe margin. Design your enrichment Lambda for fast batch processing:
// Fast enrichment pattern: parallel batch lookup with timeout guard
exports.handler = async (events) => {
const ENRICHMENT_TIMEOUT_MS = 20_000; // 20s budget — well under 29s Pipes limit
const startAt = Date.now();
// Use Promise.all for parallel DynamoDB batch lookup instead of sequential
const enriched = await Promise.all(events.map(async (evt) => {
if (Date.now() - startAt > ENRICHMENT_TIMEOUT_MS) {
// Return partial enrichment — mark as timeout rather than fail
return { ...JSON.parse(evt.body), enrichmentStatus: "timeout" };
}
return await enrichSingleEvent(evt);
}));
return enriched;
};
// Anti-pattern: sequential N+1 DynamoDB gets — each adds 1-5ms, fails at ~200 events
const sequential = async (events) => {
const results = [];
for (const evt of events) {
const tenant = await dynamodb.getItem(/* ... */).promise(); // sequential!
results.push({ ...JSON.parse(evt.body), tenant });
}
return results;
};
API Gateway enrichment
Instead of a Lambda, you can configure an API Gateway REST or HTTP API endpoint as the enrichment target. Pipes sends an HTTP POST with the batch payload and uses the HTTP response body as the enriched output passed to the target.
// CDK: API Gateway enrichment
import { RestApi, LambdaIntegration } from "aws-cdk-lib/aws-apigateway";
const enrichApi = new RestApi(this, "EnrichApi");
const enrichResource = enrichApi.root.addResource("enrich");
enrichResource.addMethod("POST", new LambdaIntegration(enrichBackendFn));
const pipe = new pipes.CfnPipe(this, "ApiEnrichedPipe", {
// ...
enrichment: enrichResource.url, // API Gateway URL
enrichmentParameters: {
httpParameters: {
pathParameterValues: [], // URL path variables (not needed for POST)
headerParameters: {
"X-Pipe-Source": "mcp-tool-calls",
"Content-Type": "application/json",
},
queryStringParameters: {
"env": "prod",
},
},
inputTemplate: "$.body", // POST body = source record body
},
// ...
});
API Gateway enrichment enforces a 5-second timeout (not 29 seconds like Lambda). This makes it suitable only for lightweight lookups or pass-through enrichment. The API response body must be a valid JSON array matching the enrichment response contract. Use Lambda enrichment for anything that requires more than trivial transformation time.
Step Functions enrichment — Express Workflows only
EventBridge Pipes supports Step Functions as an enrichment target, but with a critical constraint: only Express Workflows are supported, not Standard Workflows. Express Workflows run synchronously (via StartSyncExecution), which Pipes waits for. Standard Workflows are asynchronous (you start them and poll for completion) — Pipes has no mechanism to await asynchronous executions.
// CDK: Step Functions Express Workflow enrichment
import { StateMachine, StateMachineType } from "aws-cdk-lib/aws-stepfunctions";
const enrichmentSfn = new StateMachine(this, "EnrichSfn", {
stateMachineType: StateMachineType.EXPRESS, // REQUIRED — Standard fails silently
definition: /* your enrichment workflow */,
timeout: cdk.Duration.seconds(25), // Keep under 29s Pipes limit
});
const pipe = new pipes.CfnPipe(this, "SfnEnrichedPipe", {
// ...
enrichment: enrichmentSfn.stateMachineArn,
// No enrichmentParameters needed for Step Functions — uses default input
// ...
});
// Grant pipe role execution permission
enrichmentSfn.grantStartSyncExecution(pipeRole);
Express Workflow enrichment is useful when your enrichment logic involves complex branching, parallel lookups to multiple services, or you want the execution history for debugging. The execution output (the state machine's final state output) becomes the enriched payload passed to the target. The output must be a valid JSON array with the same structure as Lambda enrichment responses.
Enrichment failure semantics
If enrichment fails (Lambda throws, returns non-2xx, times out after 29s, or returns invalid JSON), EventBridge Pipes retries the entire batch according to the pipe's retry policy. There is no partial batch failure granularity for enrichment — unlike Lambda ESM which supports batchItemFailures to isolate individual failing records.
| Enrichment failure mode | Pipe behavior | Recovery |
|---|---|---|
| Lambda throws exception | Batch retried up to maximumRetryAttempts times |
Fix enrichment Lambda; retries resume automatically |
| Lambda times out (>29s) | Batch retried; Lambda invocation continues in background | Optimize enrichment; use idempotent writes to handle background completion |
| Lambda returns wrong JSON shape | Pipe may pass malformed data to target or fail at target stage | Validate enrichment output structure against target schema |
| Enrichment retries exhausted | Pipe enters RUNNING_FAILED; source polling stops | Fix enrichment; call aws pipes start-pipe to resume |
| Lambda returns empty array [] | Zero records passed to target for that batch — not an error | Use intentionally to filter records dynamically from enrichment |