Event workload contracts
AppTheory’s non-HTTP Lambda story uses the same single entrypoint as HTTP and AppSync:
HandleLambda, handleLambda, or handle_lambda. EventBridge, scheduled rules, SQS, Kinesis, SNS, and DynamoDB
Streams are detected by event shape and routed through the AppTheory event-source registry. Do not add a second dispatcher
for “background jobs”; grow the contract and fixtures when workload behavior needs to become portable.
This page documents the event workload contract pinned by the shared m1 fixtures. The fixtures remain the
specification for every event-source behavior. When a helper is part of the public API, use that helper instead of
copying event-shape or decoding logic into service code.
EventBridge workload envelope
EventBridge workload handlers should be able to derive a safe envelope summary with these portable fields:
event_id: EventBridgeidsource: EventBridgesourcedetail_type: EventBridgedetail-typeordetailTypeaccount,region,time, andresourcesrequest_id: Lambda invocation request IDcorrelation_idandcorrelation_source
Correlation ID precedence is fixed across languages:
event.metadata.correlation_idevent.headers["x-correlation-id"]event.detail.correlation_id- EventBridge
event.id - Lambda context
awsRequestId
metadata and top-level headers are an AppTheory portable envelope convention used by the contract fixtures and
future helpers. They are not fields that AWS EventBridge adds to native events. Producers using plain EventBridge custom
events should either put correlation data in detail.correlation_id or use an AppTheory-defined envelope before the
runtime helper reads the event. Header names are matched case-insensitively when the portable envelope is present.
If an EventBridge event does not match a registered rule name or source/detail-type selector, the existing AppTheory
behavior remains unchanged: the handler result is nil / null, not an alternate error path.
Scheduled workloads
Scheduled workloads are EventBridge events with source = "aws.events" and detail-type = "Scheduled Event". The contract
pins a structured run summary so schedule handlers do not invent per-service result shapes.
Portable summary fields:
kind: alwaysscheduledrun_id:detail.run_id, then EventBridgeid, then LambdaawsRequestIdidempotency_key:detail.idempotency_key, theneventbridge:<event.id>, thenlambda:<awsRequestId>correlation_id/correlation_source: the EventBridge precedence aboveremaining_ms: the remaining invocation time supplied by the Lambda contextdeadline_unix_ms: current runtime clock plusremaining_ms, or0when no deadline is availableresult.status: defaults tookresult.processedandresult.failed: default to0
Retry and reliability guidance:
- Treat schedules as at-least-once triggers.
- Use the idempotency key before committing external side effects.
- Configure EventBridge target retry policy, maximum event age, and DLQ through the deployment construct instead of branching inside the handler.
- Use the Lambda remaining-time/deadline fields to stop before timeout and return a structured summary when possible.
Kinesis workloads and CloudWatch Logs subscriptions
Kinesis uses the same AppTheory Lambda entrypoint as every other trigger. HandleLambda, handleLambda, and
handle_lambda detect Records[0].eventSource == "aws:kinesis" and route to the registered Kinesis stream handler:
- Go:
app.Kinesis(streamName, handler)andapp.ServeKinesis(ctx, event) - TypeScript:
app.kinesis(streamName, handler)andapp.serveKinesisEvent(event, ctx) - Python:
app.kinesis(stream_name, handler)andapp.serve_kinesis(event, ctx)
Kinesis handlers run per record. If a handler returns an error or throws, the record’s eventID is returned in
batchItemFailures. Successful records are omitted. An event for an unregistered stream fails closed by returning every
record ID as a failure, matching Lambda’s partial-batch response model.
The AppTheory-owned path for CloudWatch Logs delivered through Kinesis is:
- A CloudWatch Logs subscription filter targets an
AppTheoryCloudWatchLogsDestination. AppTheoryCloudWatchLogsDestinationdelivers records to anAppTheoryKinesisStream.AppTheoryKinesisStreamMappingwires the stream to the AppTheory Lambda consumer with partial-batch failures enabled.- The Lambda handler stays on
HandleLambda/handleLambda/handle_lambda. - The Kinesis handler decodes each CloudWatch Logs envelope with the runtime decoder before domain processing.
Runtime decoder names:
| Language | Decoder |
|---|---|
| Go | DecodeCloudWatchLogsSubscription(record) |
| TypeScript | decodeCloudWatchLogsSubscription(record) |
| Python | decode_cloudwatch_logs_subscription(record) |
The decoder understands the gzip-compressed CloudWatch Logs subscription envelope carried in the Lambda Kinesis record’s
data field. It returns typed log envelope fields plus safe_summary. Raw CloudWatch log messages are available only in
the decoded log_events / LogEvents payload for the handler’s local domain work; safe_summary intentionally contains
only record/log identity and counts.
Kinesis producer helper names:
| Language | Record helper | Failure report helper |
|---|---|---|
| Go | NewKinesisJSONRecord |
ReportKinesisPutRecordsFailures |
| TypeScript | createKinesisJsonRecord |
reportKinesisPutRecordsFailures |
| Python | create_kinesis_json_record |
report_kinesis_put_records_failures |
Use these helpers when an AppTheory workload needs deterministic JSON bytes and safe PutRecords-style failure summaries. They validate partition keys, canonicalize explicit hash keys, enforce Kinesis record bounds, align failures by input/result index, and exclude JSON payload bodies and raw error messages from safe summaries. If a service needs a broader producer abstraction, grow the AppTheory helper surface instead of inventing a per-service record or failure shape.
Deterministic testkit helpers are available for Kinesis events and CloudWatch Logs subscription records:
- Go:
KinesisEvent,KinesisCloudWatchLogsSubscriptionRecord, andCloudWatchLogsSubscriptionData - TypeScript:
buildKinesisEvent,kinesisCloudWatchLogsSubscriptionRecord, andcloudWatchLogsSubscriptionData - Python:
build_kinesis_event,kinesis_cloudwatch_logs_subscription_record, andcloudwatch_logs_subscription_data
Canonical example: examples/cdk/kinesis-cloudwatch-logs.
DynamoDB Streams workloads
DynamoDB stream handlers keep the existing Lambda partial-batch response contract. Successful records are omitted from
batchItemFailures; failed records return their eventID as itemIdentifier; an unrecognized table fails closed by
returning every record ID as a failure.
The normalized safe record summary contains only operational metadata:
table_name: parsed fromeventSourceARNevent_id:eventIDevent_name:eventNamesequence_number:dynamodb.SequenceNumbersize_bytes:dynamodb.SizeBytesstream_view_type:dynamodb.StreamViewTypesafe_log: a derived summary from table/event/sequence metadata
Keys, NewImage, and OldImage are not safe-log fields. The fixtures intentionally include raw-value sentinels such as
release#rel_123, do-not-log, and previous-secret; runtime helpers must not leak those values into logs, metrics,
spans, or safe summaries.
Retry and idempotency guidance:
- DynamoDB Streams are at-least-once. A failed
eventIDcan be delivered again. - Use the stream
eventID, sequence number, or a domain key from a trusted normalized value for idempotency. - Never log raw image values while diagnosing retries; log only the safe summary fields above.
Non-HTTP observability and safe errors
Event workloads use the same portable fixture side-effect fields as P2 HTTP fixtures: expect.logs, expect.metrics, and
expect.spans. The event workload dimensions are trigger-specific and must not include raw payloads.
EventBridge dimensions:
trigger = "eventbridge"correlation_idsourcedetail_typeerror_codeoutcome
DynamoDB Streams dimensions:
trigger = "dynamodb_stream"correlation_idset to the streameventIDtable_nameevent_idevent_nameerror_codeoutcome
The safe error posture for non-HTTP workloads is:
- observability records use
error_code = "app.internal"for non-portable handler panics or raw internal errors - surfaced errors use the safe message
apptheory: event workload failed - raw exception messages and raw event payloads are not logged or returned
Current fixture status: the m1 safe-panic fixture is contract-first emulation by the runner handler. Follow-up runtime
milestones must add or convert fixtures so a handler throws a raw/internal error and the AppTheory runtime itself performs
recovery, sanitization, and effect emission.
CDK wiring
Use AppTheory CDK constructs for deployment wiring; do not drop to bespoke raw CDK for one workload shape.
AppTheoryEventBridgeRuleTarget: schedule or EventBridge pattern to Lambda wiring. UsetargetPropsfor DLQs, retries, and maximum event age.AppTheoryEventBridgeBus: custom bus plus explicit cross-account publisher allowlist.AppTheoryDynamoDBStreamMapping: DynamoDB stream to Lambda event-source mapping.AppTheoryKinesisStream: create or wrap the encrypted Kinesis Data Stream for AppTheory event ingestion.AppTheoryKinesisStreamMapping: stream-to-Lambda event-source mapping withreportBatchItemFailuresdefaulting totrue.AppTheoryCloudWatchLogsDestination: CloudWatch Logs destination with explicit source account and/or organization allowlists; it does not synthesize a broad default destination policy.AppTheoryQueue,AppTheoryQueueConsumer, andAppTheoryQueueProcessor: SQS queue and worker patterns when a DLQ or queue boundary is the correct retry domain.AppTheoryJobsTable: job/run ledger storage for long-running workloads that need idempotency, leases, and record-level status.
The CDK constructs provide transport and IAM wiring. The handler still owns domain idempotency and must stay on the AppTheory runtime entrypoint so the event workload contract remains fixture-backed across Go, TypeScript, and Python.
For CloudWatch Logs through Kinesis, use the single AppTheory-owned chain documented above. Placeholder account IDs such
as 111122223333 and organization IDs such as o-example1234 in examples are examples only; replace them before any
real deployment. The Logs destination must be explicitly allowlisted and should fail closed when no trusted source is
configured.