Generated banner for Amazon Eventbridge Event Bus
Generated logo for Amazon Eventbridge Event Bus

Amazon Eventbridge Event Bus

AWSOfficialPublished by AWS

  • Skill
  • Tool integration
  • Cloud and DevOps
  • Apache-2.0
  • No issues found

From the author

Builds, runs, debugs, and operates event-driven applications using EventBridge Event Bus - a managed, centrally governed publish/subscribe event bus that an organization can share across many teams and accounts. Applicable when workloads need event-driven architectures, decoupling, choreography, asynchronous integration, pub/sub, fan-out, event ordering, delivered in sequence, event router, event broker, event bus, event store, event retention, event replay, deduplication, avro, protobuf, cloudevents, uses events or messages. Designed as a centrally managed event bus platform allowing governance and control - such as subscriber control, revocation, per-account throttling - while giving application owners flexibility across an organization spanning multiple accounts, with end-to-end open-standards observability and cost allocation. Also helps reduce costs in high fan-out scenarios where multi-account forwarding adds cost. A serverless, managed alternative to self-managed event streaming platforms.
— the description by AWS

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides comprehensive guidance for building, running, and debugging event-driven applications using the modern EventBridge custom event bus. It covers the lifecycle of event management, including publishing via PutEvents or PutRawEvents, configuring subscribers with filters and transformers, and managing cross-account event routing. The skill emphasizes the distinction between the new custom event bus service and classic EventBridge, providing specific instructions for handling binary payloads, CloudEvents, and complex routing requirements.

What it does

  • Guides the creation and management of custom event buses and subscribers.
  • Explains the differences between PutEvents and PutRawEvents APIs for payload handling.
  • Provides patterns for filtering, transforming, and routing events to various targets.
  • Details authorization and security policies for cross-account event sharing.
  • Offers troubleshooting steps for delivery failures, observability, and metrics.
  • Supports advanced features like event replay, deduplication, and FIFO ordering.

When to use it

  • When designing decoupled, asynchronous event-driven architectures.
  • When you need to implement fan-out patterns across multiple AWS accounts.
  • When you require a managed, serverless alternative to self-hosted event streaming.
  • When you need to integrate AWS service events or partner SaaS events into your workflow.
  • When you need to perform event transformation or filtering before delivery.

Advantages

  • Centrally governed publish/subscribe model for multi-team environments.
  • Native support for open standards like CloudEvents, Avro, and Protobuf.
  • Built-in durability with configurable event retention and replay capabilities.
  • Granular control over subscriber access and per-account throttling.
  • End-to-end observability with integrated metrics and delivery logging.

Usage examples

“Write a filter so this subscriber only gets orders over $500.”
The agent provides a JSON filter pattern scoped to the event data, ensuring only events matching the specified amount are routed to the target.
“My producer says PUBLISHED but nothing shows up in the queue. What do I check?”
The agent guides you through checking subscriber logs, verifying the target role permissions, and confirming the filter logic matches the published payload structure.
“Should I use PutEvents or PutRawEvents for Protobuf payloads?”
The agent explains that PutRawEvents is required for binary or non-JSON payloads, and provides instructions on setting the appropriate ContentType.
“Set up a subscriber that reprocesses everything from the last two days.”
The agent provides the steps to configure a replay on the event bus, specifying the retention window and the target subscriber.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/aws/amazon-eventbridge-event-bus/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip amazon-eventbridge-event-bus.zip -d .agents/skills

Documentation by AWS

Building with the new EventBridge custom event buses

Overview

The new EventBridge custom event bus routes events and retains them. You publish with one of two APIs, and subscribers filter, optionally transform, and deliver each matching event to one target.

Read chapters 5 and 6 before writing any filter, transformer, or target parameter. The two publish APIs deliver different payload shapes, and an expression written for one silently matches nothing against the other.

Publish metrics are on by default, so you can always tell whether events reached a bus. Subscriber logs are off by default, so turn them on before you debug a delivery rather than after (chapter 18).

Works best with the AWS MCP server, which is recommended for running the CLI calls in this skill: it executes them in a sandboxed environment with audit logging. All guidance also works with standard AWS CLI access.

Conventions

The key words MUST, MUST NOT, SHOULD, and MAY in this skill are to be interpreted as described in RFC 2119. They appear only in decision procedures, where following the wrong course either breaks the configuration or loses events silently. MUST marks an absolute requirement, SHOULD a strong recommendation you may have a specific reason to set aside, and MAY a genuine option.

Everything else is ordinary description. A sentence stating how the service behaves carries no keyword, because it is a fact rather than an instruction to you.

When not to use this skill

This skill is only about the new custom event buses, the service you reach with the eventsv2 CLI and the eventbridgev2 SDK clients. It does not cover:

  • Classic EventBridge, meaning the events API with rules, targets, archives, and event patterns on AWS::Events::Rule. The two services share the events: IAM namespace and some vocabulary, which is exactly why they are easy to confuse. If you are writing a rule, you are not on a new bus. Chapter 3 maps the concepts across if you are migrating.

  • EventBridge Pipes, Scheduler, and Schemas (the classic schema registry and its discovery feature). Different APIs.

  • Configuring the Firehose service itself, such as buffering hints or record transformation. A Firehose delivery stream is a valid subscriber target; chapter 9 covers that side of it.

Trigger examples

  1. "Write a filter so this subscriber only gets orders over $500."

  2. "My producer says PUBLISHED but nothing shows up in the queue. What do I check?"

  3. "Should I use PutEvents or PutRawEvents for Protobuf payloads?"

  4. "Set up a subscriber that reprocesses everything from the last two days."

  5. "Write a JSONata expression that pulls the customer id out of the event into a DynamoDB putItem."

Guardrail — where this skill's own files live (MCP vs local install)

This skill can be loaded two ways, and they resolve the skill's own bundled files from different places. Determine how the skill was loaded before reading a reference:

  • Loaded through the AWS MCP retrieve_skill tool: The skill is not installed on the local filesystem. You MUST fetch each reference via retrieve_skill with the file parameter (e.g. file="references/setup-walkthroughs.md"). Do NOT file_read these paths locally — they do not exist on disk.

  • Installed locally (e.g. .kiro/skills/amazon-eventbridge-event-bus/ or ~/.claude/skills/amazon-eventbridge-event-bus/): Read files from the local skill directory using relative paths.

This distinction applies only to the skill's own packaged files. User data and session artifacts are always read from and written to the user's working directory. Never fetch or write customer data through retrieve_skill.

Reference files

The chapters below carry the decisions and the failure modes; the detail lives in these files. Load a file when the task matches its row, and do not load them all by default.

FileWhen to load
setup-walkthroughs.mdstanding up a working bus end to end, in one account or shared across accounts with AWS RAM, including prerequisites and teardown
code-samples.mdwriting an individual CLI or SDK call (Python, Java, TypeScript): create a bus and subscriber, publish with either API, binary payloads, replay, cross-account grants, waiters, per-entry results, throttling backoff
filters-and-expressions.mdwriting any filter pattern, JSONata expression, or target parameter that reads the event; both delivered envelopes in full
target-configuration.mdconfiguring any subscriber target, especially a universal target
target-contract.mdwriting an actual CreateSubscriber call: required fields, a working example per target type, literal versus JSONata, the delivery error taxonomy
migrating-from-classic.mdmigrating from classic EventBridge, planning the migration sequence, or classic habits producing wrong new-bus config
event-sources.mdputting AWS service events or partner events on a bus with an event source, or forwarded events not arriving or not matching a filter
provisioning-and-state.mdcreating or updating a bus, or a resource stuck in an unexpected state
delivery-troubleshooting.mdan event published but never arrived, or building a test rig that counts deliveries correctly
authorization.mdwriting an IAM policy or bus resource policy, cross-account access checks, or anything that forwards events into a bus
replay-and-recovery.mdreading history rather than new events, pausing or resuming, or a replay that looks broken
observability.mdsetting up metrics, alarms, or the vended-log delivery; reading delivery logs; observing across accounts
schema-registries.mdpublishing Avro, Protobuf, or anything that is not JSON; the accepted content types; running alongside Kafka
cloudevents.mdpublishing or consuming CloudEvents, or filtering on CloudEvents attributes
security-and-sharing.mdencrypting a bus, sharing it cross-account, or tagging
cost-and-quotas.mdbilling mechanics, who pays what across accounts, shaping events for cost, or handling throttling
infrastructure-as-code.mdwriting a CloudFormation template, splitting stacks across accounts, or private connectivity

1. The three resources

Event bus. The thing you publish to. It routes events and retains them.

Subscriber. A statement of which events you want and where to send them, in one resource. It watches its bus, keeps the events matching its filter, optionally transforms each one, and delivers it to exactly one target. To send one event to several targets, create several subscribers on the same bus.

Event source. A managed feed into a bus, either AWS service events or a partner/SaaS source.

Names and identifiers:

  • A bus ARN and a subscriber ARN both end in a service-generated id after the name: arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef. Store the full ARN, not the name, because the name alone does not identify the resource and re-creating a deleted bus with the same name yields a different id.

  • Event source names are unique per account, not per bus.

  • Three names identify this service and they do not match. The CLI command and the endpoint host are eventsv2, as in aws eventsv2 create-event-bus and eventsv2.{region}.amazonaws.com. The SDK clients are eventbridgev2. IAM actions and ARNs are events, shared with classic EventBridge, as in events:PutEvents and arn:aws:events:...:event-busv2/.... See chapter 14.

2. A router with a durable log behind it

The bus routes each event to matching subscribers, and it also retains events for a configured period, set per bus with StorageConfiguration.RetentionPeriodInDays (1 to 365 days). Retention is what makes four other features possible: starting positions, pausing and resuming, replay, and a subscriber created today reading events published yesterday. The retention window is clamped at the bus's creation time, so on a young bus even a recent past timestamp can fall outside it.

3. Coming from classic EventBridge

A classic rule plus its target becomes one subscriber on the new bus. Four new-bus concepts have no classic analogue: retention on the bus itself, a subscriber starting position, pause and resume with a backlog, and two publish APIs with different payload models. That last changes how you write filters, so a classic event pattern ported unchanged works only for PutEvents. The concept mapping, the habits that carry over incorrectly, and a step-by-step migration sequence are in migrating-from-classic.md.

4. AWS service and partner event sources

An event source is a managed feed onto your bus. You create one naming a destination bus and one origin, and the service stands up the forwarding on the classic EventBridge side for you. There is no role and no delivery code: the managed path uses the service's own identity.

Configuration is discriminated and exactly one variant MUST be set:

VariantOriginRequired member
AwsServiceEventsConfigurationone AWS service, as aws.s3AwsService
PartnerEventsConfigurationa classic partner event sourcePartnerEventSourceArn

Both take an optional Pattern, which narrows what forwards and is evaluated on the classic side against the classic envelope. Your pattern MUST NOT carry a source key or a top-level account or region, because the service sets those itself to pin the forward to your account and origin.

A forwarded event arrives as the whole classic envelope. Data holds it, ContentType is application/eventbridge+json, your content is at $events.Data.detail, and the source and detail-type are in SystemMetadata as aws:Source and aws:DetailType. So a classic rule's source filter does not transfer: the equivalent is a SYSTEM_METADATA filter, {"aws:Source":["aws.s3"]}.

Only that SystemMetadata pair proves an AWS service or partner produced the event. No publish call can write either key. A raw payload can imitate the envelope exactly, so a DATA filter on the payload's source matches a forgery as readily as the real thing. You SHOULD filter on aws:Source whenever provenance matters, and MUST match its value rather than its presence, because a direct PutEvents sets the key too, from its own never-aws.-prefixed Source.

EventBusArn is immutable. Recovery splits by failure: from CREATE_FAILED or UPDATE_FAILED, UpdateEventSource converges the classic side back to your record, while from DELETE_FAILED the only recovery is another delete.

A bus owner's control is at create time. CreateEventSource is authorized against the destination bus with an events:source condition key, and RevokeResource is how an owner stops a feed that already exists (chapter 22). Prerequisites, encryption, dead-lettering, and what appears in your classic account are in event-sources.md.

5. Choosing a publish API

The choice fixes the payload shape, and the payload shape determines every filter, transformer, and target parameter you go on to write.

PutEventsPutRawEvents
Payload fieldDetail, a JSON stringData, a base64 blob
Payload must be JSONYesNo, any format
Required per entrySource, DetailTypeData, SystemMetadata
ContentTypeNot accepted; the service stamps application/eventbridge+jsonRequired, inside each entry's SystemMetadata
Classic-style fieldsResources, Time availableNot present
Your own metadataNoMetadata, up to 100 keys
Ordering and dedup inputsSystemMetadata.EventGroupId, SystemMetadata.DeduplicationIdSame two fields

Use PutEvents for JSON events where you want the classic-shaped envelope. Use PutRawEvents for binary or non-JSON payloads, your own metadata keys, or a payload delivered exactly as sent.

Both return one result entry per request entry in the same order, so you correlate by index. Each carries either a SuccessCode or an ErrorCode and ErrorMessage. SuccessCode is PUBLISHED or DEDUPLICATED, and DEDUPLICATED is a success, not an error. Code that treats every success as a new event makes deduplication look like delivery.

Publishing Avro or Protobuf requires a schema registry named on each publish request, not on the bus; a registry configured in the wrong place leaves your events undecoded. application/octet-stream needs no registry, because the service does not decode those bytes at all (schema-registries.md).

6. Addressing: where every field lives

Filters, transformers, and target parameters share one addressing model, and it is not the same for the two publish APIs. Every event is delivered under three top-level names: Data (the payload), Metadata (your own keys, PutRawEvents only), and SystemMetadata (service-assigned and ordering fields). A raw-published payload sits directly in Data; a PutEvents payload sits inside the classic envelope, one level down under detail. So the same field is $events.Data.m for one API and $events.Data.detail.m for the other, and the wrong form does not error: it resolves to nothing, the filter matches nothing, and the transformer emits nothing. If you publish through both APIs onto one bus, write expressions that work for both, or give each traffic type its own subscribers; no addressing form spans both. Both envelopes in full, with every SystemMetadata field, are in filters-and-expressions.md.

7. Filtering

A subscriber's FilterConfiguration holds a list of Filters, and an event must match every filter in the list to be delivered: the filters are ANDed, not ORed. Each filter has a Pattern and a Scope (DATA, METADATA, or SYSTEM_METADATA), one filter per scope. A DATA pattern is written against the chapter 6 shape, so one pattern cannot serve both publish APIs; each is correct for exactly one API and silently matches nothing for the other. FilterConfiguration is optional on create, and a subscriber without one receives every event on the bus. A filter that matches nothing is indistinguishable from a broken subscriber, so confirm a should-match and a should-not-match case by observing the target.

8. Transformation

A subscriber's Transformer has three types: RAW (the default) delivers the payload alone, WITH_METADATA delivers the three-part envelope, and JSONATA delivers the result of an expression. A JSONata expression is wrapped in {% %} delimiters and sees the event as $events, addressed exactly as in chapter 6. Most scalar target parameters also accept an expression in the same form. Some members refuse one and MUST be literals: an API Gateway header name, a message attribute's BinaryValue, and InvocationType. An expression is only syntax-checked at create, so its result is a delivery-time fact. A JSONATA transformer whose expression throws, or evaluates to nothing, is a customer-fault delivery failure, so you SHOULD test it.

9. Targets and their parameter blocks

InvokeConfiguration carries the TargetArn, the RoleArn, and at most one parameter block, matching the target. A universal target MUST carry its block; every typed block may be omitted. A classic bus and a Firehose delivery stream are both valid targets that take no block at all.

The service resolves the target type from the ARN and checks the invariants it owns before storing anything: that the block matches the target, that the ARN's region, partition and account are its own, and that every expression parses. It does not mirror the target API's own parameter grammars, value ranges, identifier limits, or which members that API requires — those stay with the target, so a request the target will reject is accepted at create and fails at delivery.

  1. You MUST supply the parameter block a universal target requires. A typed block may be omitted: KinesisParameters: {} is stored, and Kinesis reports the missing PartitionKey when it is called.

  2. You MUST NOT put a constant in a per-message field, because a constant deduplication id makes the target discard everything after the first message. Neither form is checked against the target's range, requiredness or FIFO applicability, so a bad or missing value is a delivery failure.

  3. You SHOULD check the target service's own rules. Existence, reachability, authorization, size after enveloping, and its API's required fields are enforced by the target, at delivery.

  4. You SHOULD confirm at the target, because a PUBLISHED response is not delivery, and neither is an HTTP 200 from a batch API, which can carry individually failed entries.

  5. You MUST attach a dead-letter queue and turn on logs before you test, and MUST grant the delivery role sqs:SendMessage on that queue, because a failure otherwise leaves no signal anywhere.

Each rule's failure mode, the exact rejection messages, and the per-target blocks are in target-configuration.md. Before writing the call itself, load target-contract.md for the required-field lists, one working example per target type, and the delivery error taxonomy.

10. Universal targets

A universal target invokes any supported AWS API directly, with no intermediate function. The target ARN has a fixed shape:

arn:{partition}:events:::aws-sdk:{service}:{apiAction}

Four things to get right, each of which fails at create time with a message that names the problem: apiAction is camelCase with a lowercase first letter (putItem); service is the SDK service id, which is not always the obvious word (Step Functions is sfn); UniversalTargetParameters.Input is required and holds the API request as JSON or a JSONata expression producing it; and BatchConfiguration is required. The service's validation text abbreviates universal target as "USI target"; the two mean the same thing.

11. Ordering and deduplication

Ordering is a subscriber property. Type is UNORDERED (no guarantee) or FIFO (ordered within an event group), and events are grouped by SystemMetadata.EventGroupId at publish time. A FIFO target enforces its own length and character limits on the group id, and an undeliverable event holds up the later events in its own group while other groups on the same subscriber keep flowing, so keep group ids bounded and do not derive them from unbounded input.

Deduplication happens at publish time, two ways. DeduplicationConfiguration.DeduplicationType: CONTENT_BASED on the request hashes the event content, and CONTENT_BASED is the only DeduplicationType value. To deduplicate on your own key instead, omit DeduplicationConfiguration and set SystemMetadata.DeduplicationId on each entry. A suppressed duplicate returns SuccessCode: DEDUPLICATED either way.

Keep the two ideas separate. Bus-level deduplication decides whether an event is accepted. A FIFO target's own deduplication id, set in the target parameter block, decides whether that target accepts the delivery. Different fields at different layers, and the second is a per-message value (chapter 9, rule 2).

12. Retries and dead-lettering

RetryPolicy has three members:

MemberRangeDefault
MaxRetryAttempts0-1855
MaxEventAgeInSeconds60-86400300
RetryStrategyALLALL

Delivery stops at whichever limit is reached first, and a failing delivery is retried until then. So a conclusion that "nothing arrived" is only safe after the MaxEventAgeInSeconds window has elapsed.

OnFailureConfiguration.Arn names an SQS queue for dead-lettering, and SQS is the only supported destination. An event source refuses a FIFO queue there, with the single message OnFailureConfiguration.Arn must name a standard SQS queue: FIFO queues are not supported as EventSource dead-letter queues; a subscriber accepts one, so the two resources differ on this point. Two further limits to plan around: a dead-letter record identifies failed events by id and timestamp and does not carry the payload, so replaying it needs the event still inside retention; and the dead-letter queue must be writable by the delivery role, or the failure has nowhere to go and the events disappear with no signal.

13. Delivery permissions

InvokeConfiguration.RoleArn is the role the service assumes to invoke your target. It MUST belong to the calling account, its trust policy MUST allow the events.amazonaws.com service principal, and that policy SHOULD condition on aws:SourceAccount and aws:SourceArn. It MUST carry the action the target requires (sqs:SendMessage, sns:Publish, lambda:InvokeFunction, kinesis:PutRecords, states:StartExecution, or whatever your universal target calls), and sqs:SendMessage on your dead-letter queue, or deliveries fail, no failure record is written, and nothing is observable.

CreateSubscriber also enforces iam:PassRole on that role, so a role you cannot pass fails with AccessDeniedException before target validation runs. The list understates four: Kinesis delivery calls PutRecords, Firehose PutRecordBatch, API Gateway needs execute-api:Invoke, an API destination events:InvokeApiDestination.

The target ARN is checked at create for shape, type, region, partition and account, but not for existence or reachability: a well-formed ARN naming nothing is stored and fails at delivery.

14. Authorization: what your caller needs

Chapter 13 covers the role EventBridge assumes to invoke your target. This chapter covers what your caller needs to reach the API.

Every action name is events:. The CLI command and the endpoint host are eventsv2, and the SDK clients are named eventbridgev2, but the IAM namespace is events, shared with classic EventBridge. You MUST NOT write eventbridgev2: or eventsv2: in an IAM policy, because neither names an action that exists, and both fail closed: the policy is accepted and grants nothing. A policy written from the command you just ran, granting eventsv2:PutEvents, is the usual way this happens. The name-by-name map is in authorization.md.

Publishing is authorized against the bus ARN, per entry, with no partial authorization: if one entry fails a condition check the whole request is denied, so a batch is only as authorized as its least authorized entry.

Creating a subscriber or an event source is authorized against the bus, which makes the bus the cross-account grant point: a bus owner decides there whether another account may attach anything.

Forwarding into a bus under a role you supply is authorized at ingestion, per event: a genuine AWS or partner event needs events:PutEvents, and anything you originated needs events:PutRawEvents. So a forwarder carrying both kinds MUST be granted both actions, because granting one silently denies half the stream. The condition keys, the per-create checks, and revocation semantics are in authorization.md.

15. Provisioning and resource state

Creating a bus is asynchronous: CreateEventBus returns with State: CREATING, and the bus becomes usable later. Use the waiter rather than a hand-rolled poll, because it fails fast on the failure states and surfaces StateReason instead of spinning to a timeout:

aws eventsv2 wait event-bus-active  --event-bus-arn "$BUS_ARN"

A bus has seven states, and the asymmetry that matters is between the two failure states: UPDATE_FAILED is recoverable in place, and CREATE_FAILED is not. A failed create permits only delete, so that bus must be deleted and recreated. Delete subscribers and event sources before the bus; a bus that still has either rejects DeleteEventBus with ResourceInUseException, and the message names which kind is blocking. Revoked subscribers and event sources do not block the delete.

Creates are idempotent through ClientToken. CreateEventBus, CreateSubscriber, and CreateEventSource each accept one, and a retry carrying the same token returns the original result rather than creating a second resource. An SDK generates a fresh token per call when you omit the field, so a retry your own code issues builds a new request, gets a new token, and creates a second resource. So you MUST supply your own ClientToken whenever a retry can cross a process, a queue, or a workflow step. The mismatch error and the naming collision are in provisioning-and-state.md.

16. Updating safely

One rule governs every update API, and getting it wrong silently discards configuration you did not intend to change:

An omitted top-level member means unchanged. A supplied configuration block replaces that whole block.

So an update that supplies Configuration or FilterConfiguration or LogConfiguration in order to change one field inside it clears every other field in that block. An event source updated with its Pattern omitted comes back with no pattern at all, widening its filter to everything, with no warning. So you MUST read the resource, modify the whole block, write the whole block back, and then read it back again, because reading it back is the only way to confirm what was stored.

17. Pausing, resuming, and replay

There is no separate replay API. Replay is a subscriber's starting position, evaluated against the bus's retained events, so anything still inside retention can be reread.

  • StartingPosition at create time is LATEST (only events published after the subscriber exists) or POINT_IN_TIME (from a point in retained history).

  • Pausing is State: STOPPED on UpdateSubscriber. The backlog accrues, bounded by retention, and ResumePosition decides whether the backlog is delivered or skipped on resume.

  • Replayed events are marked. SystemMetadata."aws:DeliveryType" reads REPLAY instead of LIVE, so a consumer can make replay idempotent.

  • A replay has a startup delay, then runs at live speed, so allow for the delay before concluding a replay is broken.

18. Observability: metrics and logs

Two surfaces tell you what the service is doing, and their defaults are opposite.

Publish metrics are on by default, in the AWS/EventsV2 namespace, on the tuple {EventBus} plus {EventBus, EventSource} for traffic attributed to an event source and {EventBus, PublisherAccount} for ingress bytes. PublishEventsApproximateCallCount against PublishEventsApproximateSuccessCallCount is the cheapest check that your producer is reaching the bus at all. The outcome metrics count calls, not events, so a call that returned a response counts as a success even when every entry in it was rejected; PublishEventsEntryCount against PublishEventsFailedEntriesCount is what says whether events landed. The EventBus dimension value is not the bus ARN; querying with the ARN returns no data and looks identical to no traffic.

Subscriber logs are off by default, and they need two things before they exist: LogConfiguration.Level raised above OFF, and a CloudWatch Logs vended-log delivery wiring the subscriber to a log group. Set both up before debugging a delivery, not after. The delivery setup, the metric list, and the EVENT_DELIVERY_ATTEMPT record fields are in observability.md.

19. Verifying delivery

A publish response tells you the service accepted the event. It says nothing about delivery. To know an event arrived, look at the target.

A healthy delivery reaches the target quickly, so an event that has not arrived promptly is probably failing rather than in flight. But "nothing arrived" is only sound after the retry window elapses, because until then a failing delivery is still being retried.

Two counting mistakes make a working setup look broken. Reading an SQS queue without deleting the messages re-reads them, so raw counts overstate deliveries. And a shared target accumulates messages from earlier runs, so use a fresh target per experiment or a marker unique to the run.

20. When it published but never arrived

Nearly every delivery problem presents identically: the publish succeeded and the target is empty. You SHOULD work the ordered procedure in delivery-troubleshooting.md; it covers eleven causes ordered by how often each is the answer.

Three shortcuts that do not require opening the file:

  • You SHOULD check PublishEventsEntryCount first (chapter 18). Zero means the problem is upstream of the bus and nothing downstream matters yet.

  • You SHOULD turn on subscriber logs. INFO with IncludePayload: FULL shows the per-attempt error and the exact target_input sent, without waiting for retries to exhaust.

  • To see the event as the service sees it, you MAY add a second subscriber with Transformer: { Type: WITH_METADATA } and no filter, pointed at a queue you own.

21. Errors and client retries

Batch responses are per entry. A publish can return success at the request level with individual entries failed, so check FailedEntryCount and every entry, not just the HTTP status.

Some malformed input fails the whole request instead of one entry. Anything caught while deserializing the request rejects the entire batch and takes the valid entries with it. Validate per entry client-side, and keep batches small enough that losing one is cheap.

Retryable errors. ConcurrentModificationException and ThrottlingException are retryable with backoff. InvalidInputException, AccessDeniedException, ResourceNotFoundException, and IdempotentParameterMismatchException are not; fix the request.

Treat a persistent 500 as a client-side wire-type problem first. The usual cause is a value sent as the wrong JSON type, such as a number where a string is expected or an ISO-8601 string where epoch seconds are. An SDK retries a 500 by contract, and retrying cannot fix a malformed request, so check your types before escalating.

22. Security, encryption, sharing, tagging

  • Encryption is EncryptionConfiguration.KmsKeyIdentifier on the bus. Your caller needs kms:DescribeKey on the key at create time, and on a CMK bus every publisher needs kms:Decrypt on that key too, because a caller who cannot read the bus's events back is not allowed to write them. An unusable key policy on the service's side settles the bus at CREATE_FAILED with StateReason: KMS_ACCESS_DENIED instead (security-and-sharing.md).

  • To share a bus with another account, use AWS RAM. Resource policies are the fallback for what RAM cannot express, and a hand-written policy is invisible to RAM, so you then own tracking it.

  • Some operations are restricted to the bus owner, including revoking a shared resource, so a permission grant alone is not always sufficient. Revocation is terminal and scoped to the one resource you name, so stopping an account rather than a resource means also denying it events:CreateSubscriber and events:CreateEventSource in the bus resource policy (authorization.md).

23. Cost, quotas, and platform integration

Four things that change design decisions. Current prices and quota values are on the EventBridge pricing page and in Service Quotas, not here.

  • Billing rounds up per KB on both ingestion and delivery, so many tiny events cost more than the byte total suggests; batching related facts into one event is cheaper.

  • "Enriched operations" are billed separately: JSONata evaluation, schema deserialization, and CONTENT_BASED deduplication. So prefer a per-entry DeduplicationId, with no DeduplicationConfiguration, when you already have an idempotency key.

  • Publishing is throttled per account across both publish APIs, so a service splitting traffic between them draws from one budget. Subscriber management has its own per-account limit, so CreateSubscriber and DeleteSubscriber can be throttled while publishing has headroom. Handle ThrottlingException with backoff (chapter 21).

  • CloudFormation covers the new buses under AWS::EventsV2::, with three types. Six subscriber properties are create-only and replacement is delete-then-create, so changing a target ARN in a template can silently drop events published during the gap.

Files

references/target-contract.md

The subscriber target contract

Load this before writing any CreateSubscriber or UpdateSubscriber call. It answers four questions: which fields you must supply, which fields the target service requires at delivery, when a literal is checked and when it is not, and how a delivery failure is reported.

target-configuration.md is the shorter companion: it gives the per-target parameter block and the five rules. This file gives the contract those rules come from, with one working example per target type.


1. Who validates what, and when

Three layers check a subscriber, in this order. Knowing which layer owns a check tells you where a mistake will surface.

Layer 1, the model. Shapes, lengths, patterns, and @required members. It runs before any service code. It is deliberately narrow about targets: TargetArn is typed as a service-neutral ARN (^arn:aws(-[a-z0-9]+)*:[^\s]+$, 1 to 1600 characters), because the supported target services use different resource separators. So the model never rejects a target for being the wrong kind of resource.

Layer 2, the service, before the subscriber is stored. This is where the service checks the invariants it owns. It resolves a target type from the ARN, then applies the rules it needs in order to route, to build a request at all, and to protect itself. Every failure here is an InvalidInputException naming the member, and nothing is persisted.

Layer 3, the target service, at delivery. Existence, reachability, authorization, value ranges, identifier grammars and lengths, sizes, and required fields of the target's own API.

The line between layer 2 and layer 3 is one rule, and it is narrower than it looks:

Layer 2 checks only what the service itself needs, plus generous anti-abuse ceilings. It does not hold a second copy of the target API's parameter grammar. Anything belonging to the target — its ranges, its identifier rules, its vocabularies — is checked by the target, when it is invoked.

That boundary is deliberate. A rule the service copies from a target is enforced twice, and the two copies drift: when the target widens a rule, the copy rejects a value the target would have accepted, and the customer gets an error for a request that was fine. So the copy is not made.

Three consequences follow, and all three are common surprises.

  1. A target ARN is checked for shape, type, region, partition and account, never for existence. A well-formed ARN naming a queue that does not exist is accepted, and becomes a delivery-time failure. A wrong region or partition is a different matter and is rejected at create: TargetArn must be in region us-east-1, TargetArn must be in partition aws. Only a new-custom-bus and a universal target are exempt from the region check.

  2. A JSONata expression is checked for syntax, not for its result. Create-time acceptance of an expression is no evidence it is correct; it is not evaluated until an event arrives.

  3. A value the target will reject is accepted in either form. A DelaySeconds of 999999 and of {% 999999 %} are both stored, and SQS rejects both at delivery with a 400 InvalidParameterValue. The accepted range is SQS's, so the service does not carry it. The same holds for a member the target requires and you omitted, and for one the target does not accept on the resource you named: as long as the SDK can serialize what you sent, the service forwards it and the target decides.

What layer 2 actually checks

Resolved from the ARN's service and resource type, so this list is also the set of supported targets:

Target ARNTypeParameter block
arn:{p}:sqs:{r}:{a}:{queueName}SQSSqsParameters
arn:{p}:sns:{r}:{a}:{topicName}SNSSnsParameters
arn:{p}:lambda:{r}:{a}:function:{name}[:{qualifier}]LambdaLambdaParameters
arn:{p}:kinesis:{r}:{a}:stream/{name}KinesisKinesisParameters
arn:{p}:states:{r}:{a}:stateMachine:{name}[:{version}]Step FunctionsStepFunctionsParameters
arn:{p}:firehose:{r}:{a}:deliverystream/{name}Firehosenone permitted
arn:{p}:execute-api:{r}:{a}:{apiId}/{stage}/{method}[/{path}]API GatewayHttpParameters
arn:{p}:events:{r}:{a}:api-destination/{name}/{id}API destinationHttpParameters
arn:{p}:events:{r}:{a}:event-busv2/{name}/{id}new custom busEventBusV2Parameters
arn:{p}:events:{r}:{a}:event-bus/{name}classic busnone permitted

An ARN whose service is not in that table is rejected with InvokeConfiguration.TargetArn: TargetArn is not a supported subscriber target. A recognised service whose resource part has the wrong shape names the shape it wanted instead, for example InvokeConfiguration.TargetArn: TargetArn resource must be of the form stream/{name}. Each form's delimiter is pinned, so deliverystream:name is rejected where deliverystream/{name} is required, and a form written without [:{qualifier}] rejects a trailing element.

Those shape rules are structural only, and no character is reserved. A queue named queue?name or queue*name is accepted, because the resource part carries neither / nor : and so parses as a bare queue name. The * in an execute-api ARN is a non-blank path segment like any other, not a wildcard. Neither is a useful thing to do; the point is that a create call will not stop you.

Given the resolved type, the service checks two groups, and nothing outside them.

1. Routing — can the service reach this target at all.

  • Every parameter block you supply must match the resolved target. Each target type accepts exactly one block, so this one rule also covers supplying two: one of the two cannot match. The message names the offending block and the expected one, for example SnsParameters is not supported for target arn:aws:sqs:us-east-1:111122223333:orders. Expected SqsParameters. A target that takes no block at all — a classic bus, or Firehose — reads Expected no target parameters. The block must match because the invoker is chosen from the ARN, so a mismatched block would be stored and never read.

  • The target is in the caller's account. Target resource account (X) must match the subscriber account (Y). Unlike the ARN messages above, this one carries no field-name prefix.

  • The target is in the endpoint's region and partition, except for a new-custom-bus or universal target, which are exempt from the region check.

  • The target is not the subscriber's own bus. InvokeConfiguration.TargetArn must not reference the subscriber's own event bus, because delivering to the source bus re-publishes and re-matches forever. The check is exact string equality, so it catches only the identical ARN and not some other bus that forwards back.

2. Rules that protect the service itself.

  • Every JSONata expression must parse, because the service evaluates it once per delivered event. An expression that fails only at delivery is invisible to the caller.

  • An API Gateway header name must be a literal, and must not be one the service signs. Authorization, Host, and any header prefixed X-Amz are rejected, case-insensitively, because the request is signed with SigV4 and overriding a signed header invalidates the signature. This is not API Gateway's reserved-header list, which API Gateway owns and enforces itself. The rule is scoped to API Gateway. An API destination also takes HttpParameters, and its header names are not checked at all — neither for being literal nor for being signing-sensitive.

  • A BinaryValue must not contain {% or %}, because it is forwarded unevaluated and an expression would arrive at the target as its own literal text. The test is for either delimiter anywhere in the value, so a literal that merely contains one is rejected too.

  • 1 MiB of UTF-8 per message-attribute map, counting each entry's name, DataType, StringValue and BinaryValue text. MessageAttributes and MessageSystemAttributes are measured separately rather than summed, and a BinaryValue counts its Base64 text rather than its decoded bytes. This is an anti-abuse ceiling well above any target's own budget, so the target is still what rejects an oversized set.

  • A message-attribute map must not hold a null attribute object. A null there is not a value the SDK can serialize; it is a hole the service would dereference while building the request.

Not checked here, because they belong to the target: whether a member the target's API requires is present, whether a member applies to the standard or FIFO resource you named, SQS's delay range and its refusal of a per-message delay on a FIFO queue, SNS's MessageStructure vocabulary, Lambda's durable-execution-name and tenant-id grammars, whether a Lambda qualifier names a version or an alias that exists, FIFO group-id and deduplication-id requiredness, emptiness, length and character rules, Kinesis's partition-key requiredness and length, message-attribute names against SQS's and SNS's own naming rules, DataType requiredness, attribute counts and data-type vocabularies, the Base64 contract, and reserved system-attribute names.

RoleArn is not checked for account here. CreateSubscriber authorization enforces iam:PassRole on it first, which rejects a role you cannot pass with AccessDeniedException before validation runs.


2. Required request fields

Unconditionally required by the model on CreateSubscriber:

MemberNotes
Name1 to 256 chars, first character alphanumeric, then [.\-_A-Za-z0-9]
EventBusArnfull ARN including the 25-character generated id
InvokeConfiguration.TargetArnsee the table above
InvokeConfiguration.RoleArnmust be passable by the caller

Required conditionally, by the service:

ConditionThen required
FilterConfiguration supplied on createat least one filter in Filters with a non-empty Pattern, so {} is rejected. Each Filter needs both Pattern and Scope. Omitting the block is allowed and receives every event
Transformer.Type: JSONATATransformer.JsonataConfiguration.Expression
StartingPosition: POINT_IN_TIMEPointInTimeConfiguration.PointType

Everything else is optional, and three optional members are worth treating as mandatory in practice: OnFailureConfiguration, LogConfiguration, and RetryPolicy. The first two are why a failure is visible at all, and every example below sets them.

Target-required runtime fields

These are the target API's own required fields. The service does not check them at create. The target service checks them at delivery — including a FIFO MessageGroupId, which the service no longer requires at create. A .fifo queue or topic with no group id is stored, and SQS or SNS rejects every delivery.

Target APIRequired by the target
SQS SendMessageBatchqueue URL, message body; MessageGroupId on a FIFO queue; a DataType on every message attribute
SNS PublishBatchTopicArn, Message; MessageGroupId on a FIFO topic; a DataType on every message attribute
Lambda Invokefunction identifier, payload
Kinesis PutRecordsPartitionKey per record, stream name
Step Functions StartExecutionstate machine ARN, input
Firehose PutRecordBatchdelivery stream name. There is no parameter block, so there is nothing else to supply

The target also decides applicability: whether a member is accepted on the resource you named. MessageDeduplicationId on a standard queue, an SNS group or deduplication id on a standard topic, and a per-message DelaySeconds on a FIFO queue are all stored and dealt with when the target is called.

For the typed target blocks the service builds the request, so you only supply the parts the block exposes.


3. Literal versus JSONata

Most scalar target parameters are modelled as strings so they can carry either a literal or a JSONata expression in {% ... %} form. The expression is evaluated once per delivered event, with the event available as $events (addressing is in filters-and-expressions.md).

Both forms are checked the same way, and it is a short check. A literal is checked only against the rules the service owns; an expression is additionally required to parse. Neither is checked against the target's own range or grammar, and neither is checked for being required or for applying to the resource you named. So for most members the create call tells you nothing about whether the value is acceptable — the target decides that at delivery.

MemberWhat the service checksWhat the target checks at delivery
SqsParameters.DelaySecondsexpression syntax onlythe accepted range, 0 to 900, and that a FIFO queue takes no per-message delay
SqsParameters.MessageGroupIdexpression syntax onlyrequiredness on a FIFO queue, emptiness, length and character rules
SqsParameters.MessageDeduplicationIdexpression syntax onlythat it applies to a FIFO queue at all, plus length and character rules
SnsParameters.MessageStructureexpression syntax onlythe accepted vocabulary, json
SnsParameters.Subjectexpression syntax onlylength and character rules
SnsParameters.MessageGroupIdexpression syntax onlyrequiredness on a FIFO topic, that it applies to the topic at all, emptiness, length and character rules
SnsParameters.MessageDeduplicationIdexpression syntax onlythat it applies to a FIFO topic at all, plus length and character rules
LambdaParameters.InvocationTypethe whole value: only EVENT and REQUEST_RESPONSE are accepted, and an expression is rejectednothing; the service converts the value to Lambda's own vocabulary at delivery
LambdaParameters.Qualifierexpression syntax onlythe qualifier grammar, and that the version or alias exists
LambdaParameters.DurableExecutionNameexpression syntax onlythe name grammar and length
LambdaParameters.TenantIdexpression syntax onlythe tenant-id grammar and length
StepFunctionsParameters.InvocationTypethe whole value: only EVENT and REQUEST_RESPONSE are accepted, and an expression is rejectednothing; the value selects StartExecution or StartSyncExecution
StepFunctionsParameters.Nameexpression syntax onlythe name grammar, and uniqueness
KinesisParameters.PartitionKeyexpression syntax onlyrequiredness, key length, and character rules
KinesisParameters.ExplicitHashKeyexpression syntax onlydecimal format and range
MessageAttributes.* name (the map key)expression syntax, but only when the value is fully delimitedthe name against its own naming rules. A name that resolves to nothing, or two names that resolve to the same string, fail the delivery
MessageAttributes.*.DataTypenothingrequiredness, and the data-type vocabulary
MessageAttributes.*.StringValueexpression syntax onlyvalue rules for the data type. An expression resolving to nothing is forwarded as a null value rather than failing
MessageAttributes.* (whole map)1 MiB of UTF-8 per map, and that no entry is a null objectthe count limit and the real size budget
InvocationTimeoutSecondsaccepted on HttpParameters for API Gateway, and on the Lambda, Step Functions and universal-target blocks; a literal is range-checked and rejected out of range, never clamped. The universal-target block takes a literal only, and rejects an expression at createan expression's resolved value is range-checked and clamped here; a universal target never sees one

InvocationTimeoutSeconds is the one range the service does hold, because the service is what makes the call and holds the connection open. It is a service-owned limit, not a copy of a target's. Three details do not follow from that: on HttpParameters it is valid for an API Gateway target and rejected for an API destination; a universal target refuses an expression outright and requires an integer; and on any other block an expression skips the create-time range check entirely and is clamped at delivery. The upper bound is resolved from service configuration at runtime rather than compiled in, so treat 30 as the documented default rather than a fixed ceiling.

Beyond the two InvocationType members and the universal target's InvocationTimeoutSeconds above, two more members must be literals and reject an expression outright:

  • HttpParameters.HeaderParameters keys, on an API Gateway target. A header name MUST be a literal so it can be inspected at all, and an expression is rejected on that ground. A name that is Authorization, Host, or X-Amz-prefixed is rejected because the request is signed with SigV4 and those are the headers the signature covers. API Gateway's own reserved-header list is longer and is enforced by API Gateway, at delivery. Neither check runs for an API destination, which takes the same block: there a header name may be an expression and may be one of the signing headers.

  • MessageAttributes.*.BinaryValue. It is forwarded unevaluated, so an expression would reach the target as its own literal text. The rejection triggers on {% or %} appearing anywhere in the value, not only on a well-formed expression.

One member is neither evaluated nor guarded: MessageAttributes.*.DataType is forwarded exactly as written and is not validated at all. An expression there is not rejected; it arrives at the target as its own text and the target rejects the data type.

Message-attribute names are evaluated per event. For SQS and SNS, both an attribute's name and its StringValue may be expressions. The failure modes differ: a name that resolves to nothing fails the whole delivery, as do two names that resolve to the same string, while a StringValue that resolves to nothing is delivered as a null value. Create-time syntax checking of a name is conditional — a malformed expression is caught only if its {% and %} delimiters are both intact. {% $events.Data fails the delimiter test, is treated as a literal name, and reaches SQS or SNS as that text.

Per-message fields belong in an expression. A message group id, a deduplication id, a partition key, and an execution name are per-message by nature. A literal deduplication id is accepted and makes the target treat every message after the first as a duplicate; the symptom is exactly one message arriving and then nothing. The canonical forms derive them from the event's own ordering fields:

"MessageGroupId":        "{% $events.SystemMetadata.EventGroupId %}"
"MessageDeduplicationId": "{% $events.SystemMetadata.DeduplicationId %}"

Limits: Transformer.JsonataConfiguration.Expression is 1 to 8192 characters and must match {% ... %} with nothing after the closing delimiter.


A create rejected on invocationType is the enum's casing

The InvocationType rows above produce the one create-time rejection people copy into tickets. Verbatim:

InvalidInputException: 1 validation error detected: Value at
'invokeConfiguration.stepFunctionsParameters.invocationType' failed to satisfy constraint: Member must
satisfy enum value set: [REQUEST_RESPONSE, EVENT]

The value is an enum, upper case: EVENT or REQUEST_RESPONSE. The rejected value is usually Event, the casing Lambda's own Invoke API uses, or a JSONata expression, which this member does not accept. The Lambda member path form is identical with lambdaParameters in the path.

4. Working examples

Nine target types. Each is a complete CreateSubscriber request body, and each sets an SQS dead-letter queue, subscriber logging, and lists the delivery-role actions. Substitute your own partition, region, and account. Read the subscriber back with DescribeSubscriber afterwards: CreateSubscriber returns identity and state only, never the stored configuration.

Every example carries this pair, so it is elided from the bodies below to keep them readable:

"OnFailureConfiguration": { "Arn": "arn:aws:sqs:us-east-1:111122223333:my-dlq" },
"LogConfiguration":       { "Level": "INFO", "IncludePayload": "FULL" }

LogConfiguration.Level defaults to OFF, so set it before you need it. The dead-letter queue ARN is shape-checked at create, and its queue-name grammar is SQS's own: 1 to 80 characters of [A-Za-z0-9_-], where a .fifo suffix counts toward the 80.

Every role below also needs sqs:SendMessage on the dead-letter queue. Granting the target but not the dead-letter queue loses the failure entirely: deliveries fail, the failure record cannot be written, and no surface records it.

Standard SQS queue
{
  "Name": "orders-to-queue",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:sqs:us-east-1:111122223333:orders",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role",
    "SqsParameters": {
      "DelaySeconds": "0",
      "MessageAttributes": {
        "orderId": { "DataType": "String", "StringValue": "{% $events.Data.detail.orderId %}" }
      }
    }
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"orderId\":[{\"exists\":true}]}}" } ] },
  "RetryPolicy": { "MaxRetryAttempts": 5, "MaxEventAgeInSeconds": 300 }
}

Role: sqs:SendMessage on the queue. Delivery is SendMessageBatch. SqsParameters is optional on a standard queue; omitting it delivers the payload with no attributes. MessageDeduplicationId is FIFO-only, and SQS is what says so: setting it here is accepted at create and fails at delivery. The attribute count limit, the requiredness of each attribute's DataType, the accepted data types, and the real size budget are SQS's and are enforced when the queue is called; the service applies only a generous aggregate byte ceiling and a rejection of a null attribute object. The accepted MessageSystemAttributes names are SQS's too — AWSTraceHeader is the one it takes.

FIFO SQS queue
{
  "Name": "orders-to-fifo",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "Type": "FIFO",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:sqs:us-east-1:111122223333:orders.fifo",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role",
    "SqsParameters": {
      "MessageGroupId": "{% $events.SystemMetadata.EventGroupId %}",
      "MessageDeduplicationId": "{% $events.SystemMetadata.DeduplicationId %}"
    }
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"orderId\":[{\"exists\":true}]}}" } ] }
}

Role: sqs:SendMessage on the queue. The service accepts any subscriber Type for this target. SQS still requires MessageGroupId and refuses per-message DelaySeconds on a FIFO queue, so an incompatible configuration fails during delivery. The group id above is not optional in practice; SQS is what enforces it.

SNS topic
{
  "Name": "orders-to-topic",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:sns:us-east-1:111122223333:orders",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role",
    "SnsParameters": {
      "Subject": "New order",
      "MessageAttributes": {
        "orderId": { "DataType": "String", "StringValue": "{% $events.Data.detail.orderId %}" }
      }
    }
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"orderId\":[{\"exists\":true}]}}" } ] }
}

Role: sns:Publish on the topic. Delivery is PublishBatch. Nothing about the FIFO pairing is checked at create. On a standard topic a MessageGroupId or a MessageDeduplicationId is accepted and SNS rejects the publish; on a .fifo topic an absent MessageGroupId is accepted and SNS rejects the publish. The accepted attribute data types, each attribute's DataType requiredness, and the total size budget are SNS's own, so MessageStructure and an attribute type are both create-time no-ops here.

Size is the binding constraint here, and it fails silently. SNS caps the enveloped message at 256 KB, so a payload at or above roughly 250 KB is accepted by the publish call and never arrives. Around 200 KB delivers normally.

Lambda function
{
  "Name": "orders-to-lambda",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:lambda:us-east-1:111122223333:function:process-order",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role",
    "LambdaParameters": { "InvocationType": "EVENT", "Qualifier": "$LATEST" }
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"orderId\":[{\"exists\":true}]}}" } ] },
  "BatchConfiguration": { "MaxBatchSize": 10, "MaxBatchWindowInSeconds": 5 }
}

Role: lambda:InvokeFunction on the function. Lambda owns the ARN resource subtype, region, partition, qualifier, DurableExecutionName, and TenantId. The service accepts their literal values after shape and expression syntax checks; Lambda rejects unsupported combinations during delivery. InvocationType is the exception: the service accepts only EVENT or REQUEST_RESPONSE at create, and delivers the value in Lambda's own vocabulary.

A Lambda returning batchItemFailures in a 200 response retries only the reported entries (§5).

Kinesis data stream
{
  "Name": "orders-to-stream",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:kinesis:us-east-1:111122223333:stream/orders",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role",
    "KinesisParameters": { "PartitionKey": "{% $events.Data.detail.customerId %}" }
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"customerId\":[{\"exists\":true}]}}" } ] }
}

Role: kinesis:PutRecords on the stream. KinesisParameters may be omitted or empty. The invoker still constructs PutRecords, and Kinesis owns PartitionKey requiredness plus both members' formats and ranges. Missing or invalid values are reported through retries, the DLQ, and, when enabled, subscriber logs.

Step Functions state machine
{
  "Name": "orders-to-sfn",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:states:us-east-1:111122223333:stateMachine:order-workflow",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role",
    "StepFunctionsParameters": {
      "InvocationType": "EVENT",
      "Name": "{% 'order-' & $events.Data.detail.orderId %}"
    }
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"orderId\":[{\"exists\":true}]}}" } ] }
}

Role: states:StartExecution, plus states:StartSyncExecution if you set InvocationType: REQUEST_RESPONSE. Name must be unique per account, region, and state machine, so derive it from the event. A literal Name causes every execution after the first to be rejected by Step Functions as a duplicate.

HTTP: API destination and API Gateway
{
  "Name": "orders-to-api",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:events:us-east-1:111122223333:api-destination/partner-api/exampleid0123456789abcdef",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role",
    "HttpParameters": {
      "HeaderParameters": { "x-partner-tenant": "{% $events.Data.detail.tenantId %}" },
      "QueryStringParameters": { "source": "eventbridge" },
      "PathParameterValues": [ "{% $events.Data.detail.orderId %}" ]
    }
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"tenantId\":[{\"exists\":true}]}}" } ] }
}

Role: events:InvokeApiDestination on the API destination ARN. PathParameterValues are substituted in order into the endpoint path's placeholders. Header names and values may both be expressions here, because the header rules below are enforced for API Gateway only.

For API Gateway, change TargetArn to arn:aws:execute-api:us-east-1:111122223333:{apiId}/{stage}/POST/orders, grant execute-api:Invoke, and you may add "InvocationTimeoutSeconds": "20". Two differences bite here:

  • Within HttpParameters, InvocationTimeoutSeconds is valid for API Gateway and rejected for an API destination: InvocationTimeoutSeconds is only supported for API Gateway HTTP targets. The member also exists on the Lambda, Step Functions and universal-target blocks, so that message is about this block rather than about the service as a whole. A literal is range-checked and rejected out of range rather than clamped, because the service holds the connection and so owns the range.

  • The service rejects only the headers it signs, case-insensitively: Authorization, Host, and any header prefixed X-Amz, and it also requires the name to be a literal so it can check it. It signs the API Gateway request with SigV4, and a customer override of a signed header invalidates that signature. API Gateway's own reserved-header list is longer, and API Gateway enforces it — a name like Connection, Content-Length, TE, or X-Forwarded-For is accepted at create and dealt with by API Gateway at delivery. Neither check applies to an API destination.

Firehose delivery stream
{
  "Name": "orders-to-firehose",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:firehose:us-east-1:111122223333:deliverystream/orders",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role"
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"orderId\":[{\"exists\":true}]}}" } ] }
}

Role: firehose:PutRecordBatch on the delivery stream. Delivery is PutRecordBatch. Firehose takes no parameter block, so InvokeConfiguration is exactly TargetArn and RoleArn. Supplying any block is rejected: SqsParameters is not supported for target arn:aws:firehose:... Expected no target parameters. Everything outside InvokeConfiguration still applies — filter, transformer, BatchConfiguration, dead-letter queue, logging, ordering Type, StartingPosition, RetryPolicy. The ARN's resource part must be exactly deliverystream/{name}: the colon form is rejected, as is a trailing qualifier. Batches are capped at 500 records and about 4 MiB.

Two cautions specific to this target. Records are written as the raw payload bytes with no separator, newline, or envelope added, so a downstream consumer that expects newline-delimited JSON will see concatenated records unless the payload itself ends in a newline. And this target has less operational mileage than the others here, so verify a delivery end to end before depending on it.

Another new custom event bus
{
  "Name": "orders-to-bus",
  "EventBusArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/exampleid0123456789abcdef",
  "InvokeConfiguration": {
    "TargetArn": "arn:aws:events:us-east-1:111122223333:event-busv2/downstream/exampleid0123456789abcdef",
    "RoleArn": "arn:aws:iam::111122223333:role/my-delivery-role",
    "EventBusV2Parameters": {
      "Metadata": { "forwardedBy": "orders-to-bus" },
      "SystemMetadata": { "EventGroupId": "{% $events.SystemMetadata.EventGroupId %}" },
      "DeduplicationConfiguration": { "DeduplicationType": "CONTENT_BASED" }
    }
  },
  "FilterConfiguration": { "Filters": [ { "Scope": "DATA", "Pattern": "{\"detail\":{\"orderId\":[{\"exists\":true}]}}" } ] }
}

Role: events:PutEvents and events:PutRawEvents on the destination bus. Forwarding is authorized per event at ingestion against the destination bus's resource policy, and the action is chosen from the event's own system metadata: a genuine AWS or partner event needs events:PutEvents, and anything you originated needs events:PutRawEvents. A forwarder carrying both kinds and granted only one silently loses half the stream. See authorization.md.

Metadata holds at most 100 keys. A target that is verbatim the subscriber's own bus is rejected at create, because it would loop; a different bus that forwards back to this one is not detected, so you own avoiding that cycle.


5. Target and runtime error taxonomy

Delivery failures fall into classes, and the class decides whether the event is retried, dead-lettered, or dropped. Working through the classes in order is faster than reading a message cold.

Where the failure happens

The fault column says who can act on the failure. The retry column says what the delivery path does, and the important part is that it is almost the same for every row: failures are retried by default, and being permanent does not exempt one.

ClassTriggerFaultRetried?
Transformationa JSONATA transformer's expression throws: syntax, runtime, or a resource limitcustomeryes, on the budget
Transformationthe expression evaluates cleanly and produces nothingcustomeryes, on the budget
Transformationthe event body will not parse as JSON, so $events cannot be builtcustomeryes, on the budget
Input evaluationa target-parameter expression throws against the eventcustomeryes, on the budget
Input evaluationa message-attribute name expression resolves to nothing, or two names resolve to the same stringcustomeryes, on the budget
Input evaluationthe resolved request omits a field the target API requirescustomeryes, on the budget
Local SDK conversionthe resolved request cannot be converted into an SDK request, or exceeds the target's payload cap before any call is madecustomeryes, on the budget
Target 4xxthe target rejects the call: value out of range, resource not found, access denied, malformed requestcustomeryes, on the budget
Target 429 or throttlingthe target is rate-limitingneitheryes
Target 5xxthe target service failedtargetyes
Network faultconnection, TLS, or timeoutneitheryes
Service faultthe failure is internal to this serviceserviceindefinitely, never exhausts
HTTP 200 with per-entry failuresa batch API returns success with a failed-entry listper entryonly the failed entries

Four of these are worth spelling out.

A permanent fault is retried anyway. There is no class that dead-letters on the first attempt on its own. A 4xx, an expression that throws, and a request that cannot be converted are each retried until MaxRetryAttempts or MaxEventAgeInSeconds is reached, and only then dead-lettered. Retrying cannot fix any of them, so the retries buy nothing and cost the whole window. If you want a first-attempt dead-letter — when testing a failure path, or for a target whose 4xx you know is final — set MaxRetryAttempts: 0.

A service fault is the one class that never exhausts. It retries indefinitely rather than dead-lettering, on the reasoning that the customer cannot act on it and the event should not be discarded for a defect on this side.

HTTP 200 is not success for a batch target. Delivery to SQS, SNS, Kinesis, and Firehose uses the batch APIs — SendMessageBatch, PublishBatch, PutRecords, PutRecordBatch — and each can return HTTP 200 carrying a list of individually failed entries. The invoker reads that list and retries the failed entries alone, so a partially failing batch neither succeeds nor fails as a unit. The same shape exists for Lambda: a function returning batchItemFailures in its 200 response has only those items retried. When you are counting deliveries, count at the target, never from an HTTP status.

A transformation or conversion failure never reaches the target. It is classified before the wire call, so there is no http_status and no target request id in the log record. An empty details.target_request_id on a failed attempt is the signal that the failure was local. A transformation failure is reported as its own EVENT_TRANSFORMATION_FAILURE record rather than a delivery attempt.

Retries and dead-lettering

RetryPolicy has three members, and delivery stops at whichever limit is reached first:

MemberRangeDefault
MaxRetryAttempts0 to 1855
MaxEventAgeInSeconds60 to 86400300
RetryStrategyALLALL

With the defaults, a failing delivery is attempted MaxRetryAttempts + 1 times and stops at whichever limit is reached first, so "nothing arrived" is only a sound conclusion once MaxEventAgeInSeconds has elapsed — for a permanent fault as much as a retriable one. The attempts cap is evaluated before the age cap, so when both are hit the record reports MaximumRetryAttempts.

OnFailureConfiguration.Arn names the SQS queue that receives the failure record, and SQS is the only supported destination. Read the subscriber back and confirm the field is populated; other nestings of it are accepted and stored empty.

The dead-letter record

One record per failed batch, carrying a reference per event:

{
  "version": "1.0",
  "busArn": "arn:aws:events:us-east-1:111122223333:event-busv2/my-bus/...",
  "subscriberArn": "arn:aws:events:us-east-1:111122223333:subscriber/orders-to-queue/...",
  "targetArn": "arn:aws:sqs:us-east-1:111122223333:orders",
  "errorCode": "ACCESS_DENIED",
  "errorMessage": "User: ... is not authorized to perform: sqs:sendmessage on resource: ...",
  "exhaustedRetryCondition": "MaximumRetryAttempts",
  "retryAttempts": 5,
  "failedMessages": [
    {
      "eventId": "...",
      "eventGroupId": "g9",
      "deduplicationId": "order-1234",
      "timestamp": "2026-09-07T01:43:53.768Z",
      "targetRequestId": "..."
    }
  ]
}

How to read it:

FieldUse
errorCodethe class. CUSTOMER_VALIDATION collapses every customer fault, so the class alone does not tell you which
errorMessagethe target service's own words, usually the answer. Truncated at 1024 characters
exhaustedRetryConditionMaximumRetryAttempts or MaximumEventAgeInSeconds, so you know which limit ended it
retryAttemptshow many retries happened. 0 means the budget was MaxRetryAttempts: 0, or the age limit ended it at the first attempt — it does not mean the fault was permanent, because permanent faults are retried too
failedMessages[].eventIdwhich event, for correlating against the log record and the publish response
failedMessages[].targetRequestIdthe target's own request id. Empty when the failure was local

Two limits. failedMessages carries no payload, so the record alone cannot reconstruct the event; you need the event still inside the bus's retention window. And deduplicationId is populated whenever the event carried one, including for an UNORDERED subscriber — ordering mode and publish-time deduplication are independent, so its presence says nothing about Type.

Vended logs

Set LogConfiguration.Level to INFO and reproduce, rather than waiting out the retry window. Each attempt produces one EVENT_DELIVERY_ATTEMPT record per event; details.target_input shows what the target actually received, and error.error_code with error.error_message is the same pair the dead-letter record carries. Full field list in observability.md.


6. Troubleshooting flow

Work down. Each step either explains the failure or eliminates a class.

  1. Did CreateSubscriber succeed? If the failure is an InvalidInputException, the message names the member. Fix that member; nothing was stored.

  2. Wait past the retry window. More than 5 minutes on the defaults, or past MaxEventAgeInSeconds. Before that, a failing delivery is still being retried.

  3. Confirm events reached the bus. PublishEventsEntryCount in AWS/EventsV2. Zero means the problem is upstream and nothing below matters. The EventBus dimension value is {name}/{generatedId}, not the ARN.

  4. DescribeSubscriber. Check State is RUNNING, and check Revoked. A revoked subscriber delivers nothing while still reading RUNNING, and revocation is terminal.

  5. Is anything failing at all? Turn on logs at INFO with IncludePayload: FULL, reproduce, and read details.outcome.

    • No records at all — the event is not reaching the subscriber. The cause is the filter, the starting position, or the addressing asymmetry between the two publish APIs. Go to delivery-troubleshooting.md steps 4 and 5.

    • Records with outcome: FAILURE — continue.

  6. Read error.error_code and error.error_message, then classify with §5. Do not use the attempt count to tell a permanent fault from a retriable one: both are retried on the same budget. The message distinguishes them.

    • A 5xx or a throttle: the target is unhealthy or rate-limited. Raise MaxRetryAttempts, or fix the target.

    • CUSTOMER_VALIDATION: your request or expression. Read the message; retries will not help, so MaxRetryAttempts: 0 gets you the record faster while you iterate.

  7. Read details.target_input. This settles every expression question, because it is what the target received rather than what you meant to send. An empty or missing field there is an addressing mistake in your expression, not a target problem.

  8. Empty details.target_request_id on a failure means the call never left: an input-evaluation or local-conversion fault. The expression or the resolved request is wrong.

  9. AccessDenied in the message means the delivery role is missing the target's action — or, if nothing is observable anywhere, missing sqs:SendMessage on the dead-letter queue.

  10. Still nothing. Add a second subscriber with Transformer: { Type: WITH_METADATA }, no filter, and an SQS queue you own. That delivers the event exactly as the service holds it and settles the addressing question outright.


7. Authoritative AWS references

Limits and quotas change, so read them at the source rather than from a copy. Each target's own API reference is where the required fields and value ranges live, and each service's quota page is where the numbers live.

Target APIs the service calls
Quotas and limits
Reference material for the surrounding concepts

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
AWS
Publisher
AWS (Official)
Source
aws/agent-toolkit-for-aws · skills/specialized-skills/serverless-skills/amazon-eventbridge-event-bus · commit 0a218f8
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • NOTICE (added by AutoGineer from NOTICE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by AWS or AWS.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download amazon-eventbridge-event-bus.zip