AI EngineeringZero to ProductionHome·About·What’s new·Contact
AWS AI Automation · Chapter W13

Serverless orchestration

The automation payoff is things happening without you. Lambda runs the code, EventBridge routes events, and Step Functions sequences multi-step AI pipelines with retries.

⏱️ ~2 hours🧪 3 labs🎯 Advanced
⚙️ To run this for realThe code here is complete and correct as written. To actually execute it you'll need:
  • AWS credentials (aws configure) + pip install boto3
Reading and learning works without any of this — run when you're ready. IDs/ARNs in examples are placeholders; swap in your own.

Learning objectives

  • Trigger AI processing from an S3 upload with Lambda + EventBridge.
  • Orchestrate a multi-step AI pipeline with Step Functions.
  • Handle the async services (Textract/Transcribe) without polling by hand.
  • Reason about retries, error paths, and idempotency in AI automation.
▶ Runnable companionEvery code block in this lesson is also a standalone file under code/aws13-serverless-orchestration/ in the course, with a README on how to run it. Read here, run there — no need to copy-paste.

Event-driven AI advanced

The automation payoff is things happen without you. A document lands in S3 → a pipeline extracts, classifies, and stores it, with no human trigger. Lambda runs the code, EventBridge routes the events, and Step Functions sequences multi-step flows with built-in retries.

Picture the version you're trying to escape. You wrote an AI pipeline that works beautifully — you run it by hand, it downloads the file, calls Textract, calls Claude, writes the row. So you put it on a cron job that fires every five minutes, wakes up, lists the bucket, and asks "anything new?" Most of the time the answer is "no," and you've paid for a server to sit there asking a question with no answer. Worse: when 500 invoices arrive at 9am on the 1st of the month, your one poor script processes them one at a time while the queue backs up, and if the box crashes halfway through you have no idea which files it already handled. That whole shape — a machine you keep running, polling for work it usually doesn't have, that doesn't scale and doesn't remember — is exactly what event-driven serverless deletes.

The mental shift is from pull to push. Instead of your code repeatedly asking "did anything happen?", AWS tells your code the instant something does. The upload itself becomes the trigger. Nothing runs — and nothing is billed — until a real event arrives, and then as many copies as needed run at once. That's the deep meaning of "serverless": not that there are no servers, but that you never provision, scale, or babysit one. You hand AWS a function and a rule for when to run it, and it handles the rest.

Three services do the heavy lifting, and it helps to hold each one's job clearly in your head before the labs:

The common beginner mistake here is reaching for these too early — wiring five Lambdas and a state machine for a job a single script could do. Event-driven design earns its complexity when work is bursty, multi-step, and must not be lost; for a steady trickle of simple tasks, a plain queue or scheduled job is often the honest answer. The rest of this chapter shows the shape you graduate to, and when it's worth it.

from PULL (keep asking) to PUSH (get told) — nothing runs until an event arrives Polling (pull) cron server · always on every 5 min: "anything new?" S3 bucket ask "no" pays to idle · one-at-a-time · forgets on crash Event-driven (push) upload to S3 EventBridge the wiring Lambda the worker Step Fns the conductor event sequences scales to zero · runs many in parallel Stop asking; get told. The left is a cron server you pay to keep running while it repeatedly asks S3 for work; the right lets the upload itself push an event, so EventBridge, Lambda, and Step Functions run only when there is something to do.
🗺️ How to read this diagram

The dashed line down the middle splits the same job into two designs — the wasteful "pull" world on the left, the serverless "push" world on the right.

  • On the left (red), a cron server is always on. The two dashed arrows are its loop: it repeatedly asks the S3 bucket "anything new?" and usually hears "no" — you pay for that idling, it handles files one at a time, and it forgets its place if it crashes.
  • On the right, the blue upload box is the trigger: the arrival of a file pushes an event (solid blue arrows) into EventBridge (the wiring), which invokes Lambda (the worker), while Step Functions (the conductor) sequences the multi-step flow.
  • The payoff line under the right side is the "aha": nothing runs — and nothing is billed — until an event arrives, and then as many Lambda copies as needed run at once.

In short: polling keeps a server busy asking a question with no answer; event-driven serverless only wakes when a real event pushes work to it.

S3 → Lambda trigger advanced

This is the single most common serverless pattern in AWS, and it's worth understanding down to the mechanics. The story is always the same: a file appears in a bucket, and you want something to happen — extract its text, resize the image, index it, kick off a pipeline. The question is how your code learns the file arrived. The naive answer is to poll the bucket on a timer, which we just saw is wasteful and slow. The serverless answer is a bucket notification: you configure S3 once with a rule — "when an object is created here, invoke this Lambda" — and from then on the upload itself calls your function, within moments, automatically.

The piece that trips people up is what the Lambda actually receives. AWS doesn't hand your function the file — it hands it a small JSON event describing what happened. For an S3 trigger that event carries the bucket name and the object key (the file's path within the bucket), buried in a fixed nested shape: event["Records"][0]["s3"]. There can be more than one record, which is why it's a list. Your first job in almost every S3-triggered Lambda is to reach into that structure and pull out the bucket and key — and then, if you need the bytes, call S3 yourself to fetch them. Learn that event shape cold; you will type it hundreds of times.

One subtlety worth flagging before the lab, because it's a real production gotcha: object keys arrive URL-encoded in the event. A file literally named Q3 report.pdf shows up as Q3+report.pdf or Q3%20report.pdf. If you pass that raw string back to S3 or Textract, the service looks for a file with a plus sign or percent-twenty in its name and reports "not found" — a bug that only surfaces once a real user uploads a file with a space or a non-ASCII character. That's why the lab runs the key through urllib.parse.unquote_plus before using it.

The other idea the lab quietly teaches is don't wait when you don't have to. Reading a large multi-page document is slow, and a Lambda that blocks while Textract grinds is a Lambda you're paying for by the millisecond to do nothing. So instead of waiting, this handler starts the analysis and returns immediately, letting AWS notify a second function when the result is ready. Fire-and-hand-off, not sit-and-wait — that instinct is what separates a serverless design that scales cheaply from one that quietly runs up a bill.

Lambda A starts the job and returns; SNS wakes Lambda B when Textract is done S3 upload object created Lambda A reads bucket + key Textract async analysis event start returns jobId no waiting SNS topic "job done" on finish Lambda B handles result notify Fire-and-hand-off, not sit-and-wait. Lambda A starts the async Textract job and returns a jobId straight away; Textract later publishes to an SNS topic, which triggers a separate Lambda B — so no function is ever paid to block while OCR grinds.
🗺️ How to read this diagram

Follow the solid blue arrows left to right along the top row, then drop down to the second row where the result comes back later.

  • The top row is the trigger chain: an S3 upload fires an event that invokes Lambda A (green = your worker code), which pulls the bucket and key out of the event and calls Textract to start an async analysis.
  • The dashed grey arrow down to the amber returns jobId box is the key move: Lambda A does not wait — it returns the tracking id immediately (marked "no waiting") and stops being billed.
  • The bottom-right chain is what happens later: when Textract finishes it publishes to the purple SNS topic ("job done"), which triggers a second function, Lambda B, to handle the extracted result — event-driven, with no polling loop.

In short: one function kicks off the slow work and leaves; a notification wakes a different function to finish the job when the result is actually ready.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab W13.1
s3_trigger.py# Lambda triggered by an S3 put. It kicks off async Textract and returns;
# a second trigger (Textract -> SNS -> Lambda) handles the result.
import boto3, urllib.parse
tx = boto3.client("textract")

def lambda_handler(event, context):
    rec = event["Records"][0]["s3"]
    bucket = rec["bucket"]["name"]
    key = urllib.parse.unquote_plus(rec["object"]["key"])

    job = tx.start_document_analysis(
        DocumentLocation={"S3Object": {"Bucket": bucket, "Name": key}},
        FeatureTypes=["FORMS", "TABLES"],
        NotificationChannel={
            "SNSTopicArn": "arn:aws:sns:us-east-1:123456789012:textract-done",
            "RoleArn": "arn:aws:iam::123456789012:role/textract-sns",
        },
    )
    return {"jobId": job["JobId"]}
▶ How this works

This is a Lambda function — a small piece of code AWS runs for you, automatically, with no server to manage. It runs the instant a file is uploaded to an S3 bucket (S3 is AWS's file storage). Its job here is to start reading the document, then get out of the way: document analysis can take a while, so it kicks off the work and returns immediately instead of waiting.

  1. tx = boto3.client("textract") — boto3 is the AWS toolkit for Python. This line opens a connection to Textract, the AWS service that reads text, forms, and tables out of documents (PDFs, scans).
  2. def lambda_handler(event, context): is the entry point AWS calls when the function fires. AWS hands it an event — a bundle of data describing what happened. For an S3 upload, that event contains which bucket and which file.
  3. The next three lines dig the details out of the event: the bucket name and the file key (its path/name). unquote_plus undoes URL-encoding so a name like my%20file.pdf becomes my file.pdf.
  4. tx.start_document_analysis(...) tells Textract "start reading this file." FeatureTypes=["FORMS", "TABLES"] asks it to also pull out form fields and tables, not just plain text.
  5. The NotificationChannel block is the clever part: instead of waiting, we tell Textract "when you're done, post a message to this SNS topic" (SNS is AWS's notification/messaging service). A separate Lambda listens on that topic and handles the finished result later.

What the output means: The function returns {"jobId": job["JobId"]} — just a tracking number for the Textract job. It does not return the extracted text; that arrives later via the SNS notification. Returning fast like this is normal and correct for event-driven code.

Try this: Trace what happens if two files are uploaded at once: AWS simply runs two copies of this Lambda in parallel, each with its own event. That automatic scaling is the whole point of "serverless."

No hand-rolled pollingIn W10 we noted big-document Textract is async. Here SNS notifies a second Lambda when the job finishes — event-driven, not a while True: sleep() loop. This is the production shape.

Step Functions: sequence the steps expert

A Step Functions state machine is your pipeline as JSON: extract → classify with Claude → store, with automatic retries and a catch path per state. It is the durable orchestration layer you would otherwise hand-code.

Here's the pain this section removes. One Lambda triggered by one upload is easy. But real AI pipelines are chains: extract the text, then classify it with Claude, then write the result to a database — three steps where each depends on the last, and each can fail on its own. The tempting first draft is to cram all three into one giant Lambda with nested try/except blocks, a manual retry loop, and a variable tracking "which step am I on." That code is fragile: if the Lambda times out at step two, you've lost all state and have no clean way to resume. And you've hand-rolled retry logic that you'll get subtly wrong. Chaining steps reliably, across failures, is genuinely hard — and it's exactly the problem AWS packaged up as Step Functions.

The key mental move is that a Step Functions workflow is data, not code. You don't write a program that calls the steps; you write a JSON document — in a dialect called Amazon States Language (ASL) — that describes the steps and the arrows between them, and AWS executes that description for you. Think of it as a flowchart you can version-control. Each box in the flowchart is a state: most are Task states that run one Lambda, and each state names its successor with Next (or declares itself the finish with End: true). AWS runs the states in order, remembers exactly where each execution is, and shows you a live diagram lighting up as the run progresses.

The reason this earns its own service — rather than being a fancy loop — is two per-state safety features that would otherwise be scattered, error-prone code:

The distinction between those two is the thing to internalize: Retry says "try the same thing again," Catch says "give up on this path and go somewhere else." Get them backwards — catching what you should retry — and a momentary blip permanently dead-letters a perfectly good document. Reading them in the JSON below, notice that failure handling lives in configuration, declaratively, right next to the step it protects — not buried in imperative try/except code three functions deep.

the happy path runs left to right; Retry loops in place, Catch diverts down Extract Task · Lambda Classify Task · Claude Store End: true Next Next Retry ×3 Catch (any error) DeadLetter park for a human Retry loops in place; Catch diverts elsewhere. Each box is a state; solid arrows are the Next happy path Extract → Classify → Store. The curved arrow over Extract is a Retry (same step again); the arrow down from Classify is a Catch sending any error to a DeadLetter state instead of crashing the run.
🗺️ How to read this diagram

Read the three top boxes left to right — that's the pipeline's happy path — then notice the two failure arrows that leave it.

  • The solid blue arrows labelled Next are the normal flow: Extract → Classify → Store. The green Store box ends the run (End: true).
  • The curved dashed arrow looping back onto Extract is Retry ×3: a transient failure re-runs the same state up to MaxAttempts — reliability with no code from you.
  • The dashed arrow dropping down from Classify to the red DeadLetter box is Catch: on any error it abandons the happy path and parks the document safely for a human, instead of crashing the whole pipeline.

In short: Retry curls back to try the same step again; Catch branches off to a safe dead-end — and both live in configuration right beside the step they protect.

Lab W13.2
pipeline.asl.json{
  "Comment": "Doc intelligence pipeline",
  "StartAt": "Extract",
  "States": {
    "Extract":  { "Type": "Task", "Resource": "arn:aws:lambda:...:extract",
                  "Retry": [{"ErrorEquals": ["States.ALL"], "MaxAttempts": 3}],
                  "Next": "Classify" },
    "Classify": { "Type": "Task", "Resource": "arn:aws:lambda:...:classify-with-claude",
                  "Catch": [{"ErrorEquals": ["States.ALL"], "Next": "DeadLetter"}],
                  "Next": "Store" },
    "Store":    { "Type": "Task", "Resource": "arn:aws:lambda:...:store", "End": true },
    "DeadLetter": { "Type": "Task", "Resource": "arn:aws:lambda:...:dlq", "End": true }
  }
}
▶ How this works

This is not program code — it's a configuration file (JSON) that describes a Step Functions state machine. Think of it as a flowchart written as text: AWS reads it and runs your steps in order, one after another, keeping track of where it is. Each step is a "state." This one wires up a document pipeline: Extract → Classify → Store, with a safety exit if something breaks.

  1. "StartAt": "Extract" names the first step. "States" then lists every step by name.
  2. Each step has "Type": "Task" (do a unit of work) and a "Resource" — the ARN (AWS's unique ID) of the Lambda function that does that step's work. So Extract runs one Lambda, Classify runs another (classify-with-claude), and so on.
  3. "Next": "Classify" is the arrow between steps: after Extract finishes, go to Classify. The last real step uses "End": true to stop.
  4. "Retry" on Extract means: if it fails, automatically try again up to "MaxAttempts": 3 times before giving up. You get this reliability for free — no retry code to write.
  5. "Catch" on Classify is the safety net: "ErrorEquals": ["States.ALL"] means "on any error, jump to DeadLetter" instead of crashing the whole pipeline. DeadLetter is a step that parks failed documents somewhere safe for a human to look at.

What the output means: Nothing prints — this file is uploaded to AWS as the definition of the pipeline. When you later start an execution, AWS walks these states in order and shows you a live diagram of which step is running, succeeded, retried, or failed.

Try this: Follow the two possible paths with your finger: the happy path is Extract → Classify → Store → End; the failure path is Classify → (error) → DeadLetter. Every state either points to a "Next" or says "End": true.

Invoke and observe expert

You've described a pipeline; now you need to run one and watch it. In steady-state production the pipeline usually starts on its own — an upload event kicks it off — but you'll constantly want to fire a run by hand: to test a change, to reprocess a specific document, or to trigger the flow from another program. Starting a Step Functions workflow is a single API call, start_execution, and understanding the two things it takes and the one thing it returns is the whole of this section.

The distinction to hold onto is state machine vs. execution. The state machine is the blueprint — the ASL definition you wrote once, identified by its stateMachineArn. An execution is one live run of that blueprint against one specific input. You can have hundreds of executions of the same state machine running at once, each processing a different document, each tracked independently. It's the difference between a recipe and the individual dinners you cook from it — one recipe, many meals, each with its own outcome.

Two details in the call matter. First, the input is a JSON string, not a Python dictionary — that's why you wrap your payload in json.dumps. Forgetting this is the classic first-run error: pass a raw dict and the SDK rejects it. Second, the call hands back an executionArn — a unique receipt for this run. That ARN is how you find the run later: paste it into the Step Functions console and you get a live graph of your states lighting up green as they succeed, amber as they retry, red if they fail into the Catch path. That visual audit trail — being able to see exactly where any given document got to, and why it failed — is a large part of why teams reach for Step Functions instead of stitching Lambdas together by hand. Observability isn't bolted on; it comes free with the orchestration.

one blueprint (stateMachineArn) → many independent runs (executionArn) State machine the ASL blueprint · stateMachineArn start_execution(input=json.dumps(…)) execution #1 invoice.pdf · own ARN execution #2 report.pdf · own ARN execution #3 scan.pdf · own ARN One recipe, many meals. The purple blueprint is the state machine you defined once; each call to start_execution spawns an independent run against one input, tracked by its own executionArn — so hundreds of documents can be in flight at once.
🗺️ How to read this diagram

Read it top to bottom: the single blueprint at the top, the API call in the middle, and the many separate runs it produces along the bottom.

  • The purple box at the top is the state machine — the ASL definition you wrote once, identified by its stateMachineArn. It is a blueprint, not a running thing.
  • The blue middle box is the call start_execution(input=json.dumps(…)); the dashed arrow shows it acting on the blueprint. Each call launches one run.
  • The three green boxes are executions — live runs of the same blueprint against different inputs (invoice, report, scan), each with its own executionArn. The fan-out of blue arrows is the payoff: many independent, separately tracked runs from one definition.

In short: the state machine is the recipe; every start_execution cooks one meal from it, and each meal has its own receipt (the execution ARN) you use to track it.

Lab W13.3
start_sfn.pyimport boto3, json
sfn = boto3.client("stepfunctions", region_name="us-east-1")

run = sfn.start_execution(
    stateMachineArn="arn:aws:states:us-east-1:123456789012:stateMachine:doc-intel",
    input=json.dumps({"bucket": "my-docs", "key": "invoice.pdf"}),
)
print("execution:", run["executionArn"])
▶ How this works

This tiny script manually starts the pipeline you defined in Lab W13.2. In production the pipeline usually starts on its own (an upload event), but this is how you'd trigger one run yourself — for testing, or from another program.

  1. sfn = boto3.client("stepfunctions", region_name="us-east-1") opens a connection to the Step Functions service in a specific AWS region.
  2. sfn.start_execution(...) says "run one instance of this pipeline now." stateMachineArn is the ID of the pipeline (the state machine from Lab W13.2).
  3. input=json.dumps({...}) is the starting data handed to the first step. json.dumps turns the Python dictionary into a JSON text string, because that's the format Step Functions expects. Here we tell it which document to process: bucket my-docs, file invoice.pdf.
  4. print("execution:", run["executionArn"]) shows the ID of this specific run so you can find it in the AWS console.

What the output means: You get one line like execution: arn:aws:states:...:execution:doc-intel:abc123. That ARN is a receipt for this run — paste it into the Step Functions console to watch the Extract → Classify → Store steps light up in real time.

Try this: Change the "key" to a different filename and start it again — each call creates a brand-new, independently tracked execution with its own ARN.

Design for idempotencyEvents can fire more than once. Make each step idempotent (e.g. key DynamoDB writes by document id) so a retry never double-processes. This is the single most common bug in event-driven AI pipelines.

Exercise W13.1 — Wire the pipeline

Context: A pipeline is only real once it survives the console: an upload triggers it end to end, and a forced failure proves the error path actually catches. This exercise wires and then breaks the flow on purpose.

Your task: Connect S3 upload → Lambda → Step Functions (extract → Claude classify → DynamoDB store). Upload a document, watch the execution in the Step Functions console, then force an error in the classify step and confirm the catch path sends it to the dead-letter state.

Requirements:

  • An S3 upload triggers a Lambda that starts the Step Functions execution
  • The state machine runs extract → Claude classify → DynamoDB store in order
  • Watch a successful execution graph in the Step Functions console
  • Deliberately force a failure in the classify state
  • Confirm the Catch routes the failed run to the dead-letter state
  • Verify the DynamoDB write only happens on the success path, not the failed one

💡 Hint: Prove both paths: one clean run that reaches the store state, and one forced-error run whose Catch diverts it to dead-letter before any write.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Parse an S3 trigger eventBeginner

Context: Event-driven pipelines usually begin when a file lands in S3. The very first thing your Lambda must do is read the bucket and key out of the event S3 delivers — the shape of that event is fixed and worth knowing cold.

Your task: Write a Lambda handler that reads the bucket name and object key from an S3 event record. Runs offline as a pure handler.

Requirements:

  • Accept the standard (event, context) handler signature
  • S3 delivers records under event["Records"]; read the first record's s3 block
  • Pull the bucket from s3.bucket.name and the key from s3.object.key
  • Return (or print) the extracted bucket and key
  • Testable offline by passing a hand-built S3-shaped event dict

💡 Hint: The event nesting is Records[0].s3.bucket.name and Records[0].s3.object.key; hard-code a sample event to test without AWS.

Show solution

S3 delivers events under event["Records"][0]["s3"]; pull bucket and object key from there.

def lambda_handler(event, context):
    rec = event["Records"][0]["s3"]
    bucket = rec["bucket"]["name"]
    key = rec["object"]["key"]
    print("new object:", bucket, key)
    return {"bucket": bucket, "key": key}

evt = {"Records": [{"s3": {"bucket": {"name": "docs"},
                          "object": {"key": "in/a.pdf"}}}]}
print(lambda_handler(evt, None))   # {'bucket':'docs','key':'in/a.pdf'}
Exercise 2 · Async Textract with an SNS notificationIntermediate

Context: Multi-page documents can't be OCR'd in one synchronous call, so Textract offers an async job that notifies you on completion via SNS — no hand-rolled polling loop, which is the serverless-friendly way to wait.

Your task: Start Textract with start_document_analysis and a NotificationChannel so SNS fires when the job completes.

Requirements:

  • Call start_document_analysis on a boto3 textract client with DocumentLocation pointing at the S3 object
  • Request the FeatureTypes you need (e.g. FORMS, TABLES)
  • Supply a NotificationChannel with an SNSTopicArn and the RoleArn Textract assumes to publish
  • Capture the returned JobId for correlating the later completion notification
  • Understand this replaces polling: SNS signals completion instead

💡 Hint: The async start returns only a JobId; the transcript of work arrives later, and the NotificationChannel is what pushes you the "done" signal.

Show solution

For multi-page docs, kick off the async job and let SNS notify you instead of hand-rolled polling.

import boto3

tx = boto3.client("textract", region_name="us-east-1")
resp = tx.start_document_analysis(
    DocumentLocation={"S3Object": {"Bucket": "docs", "Name": "in/a.pdf"}},
    FeatureTypes=["FORMS", "TABLES"],
    NotificationChannel={"SNSTopicArn": "arn:aws:sns:us-east-1:123:tx-done",
                         "RoleArn": "arn:aws:iam::123:role/textract-sns"},
)
print(resp["JobId"])
Exercise 3 · Author a Step Functions state machineAdvanced

Context: Step Functions lets you chain Lambdas into a resilient workflow declared as data (ASL JSON), with retries and error catches built in — so failure handling lives in configuration, not scattered try/except code.

Your task: Write the Amazon States Language JSON for a linear flow Extract → Classify → Store, with a retry and a dead-letter catch on the first state. Validate it offline.

Requirements:

  • Set StartAt and define each stage as a Task state under States
  • Chain the stages with Next, and mark the terminal state with End: true
  • On the Extract state, add a Retry block (e.g. States.ALL, MaxAttempts)
  • On the Extract state, add a Catch that routes failures to a dead-letter Fail state
  • Confirm it is valid JSON offline (e.g. serialize it) — no execution needed

💡 Hint: ASL is just a dict you can build in Python and json.dumps; Retry and Catch are per-state lists that give resilience with zero custom code.

Show solution

ASL chains Task states with Next/End; Retry and Catch give resilience without custom code.

import json

asl = {
  "StartAt": "Extract",
  "States": {
    "Extract": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:extract",
      "Retry": [{"ErrorEquals": ["States.ALL"], "MaxAttempts": 3}],
      "Catch": [{"ErrorEquals": ["States.ALL"], "Next": "DeadLetter"}],
      "Next": "Classify"},
    "Classify": {"Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:classify",
      "Next": "Store"},
    "Store": {"Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:store",
      "End": True},
    "DeadLetter": {"Type": "Fail", "Error": "ExtractFailed"},
  },
}
print(json.dumps(asl)[:60], "...")   # valid JSON
Exercise 4 · Start an executionExpert

Context: Once a state machine exists, you kick off a run with a single API call and a JSON input. That call returns an execution ARN you use to track and reference the specific run.

Your task: Use Step Functions start_execution to launch the state machine with a JSON input.

Requirements:

  • Call start_execution on a boto3 stepfunctions client
  • Pass the stateMachineArn of the workflow to run
  • Serialize the run input to a JSON string for the input argument
  • Read the returned executionArn that identifies this run

💡 Hint: The input is a JSON string, not a dict — json.dumps your payload; the response's executionArn handles this specific execution.

Show solution

start_execution takes the state-machine ARN and a JSON-string input and returns an executionArn.

import boto3, json

sfn = boto3.client("stepfunctions", region_name="us-east-1")
resp = sfn.start_execution(
    stateMachineArn="arn:aws:states:us-east-1:123:stateMachine:doc-pipeline",
    input=json.dumps({"bucket": "docs", "key": "in/a.pdf"}),
)
print(resp["executionArn"])
Exercise 5 · Idempotent processingProfessional

Context: Serverless triggers deliver at-least-once, so the same event can fire twice. Any step with side effects must be idempotent — keying writes by document id makes a re-delivery a harmless no-op instead of a double-post.

Your task: Implement process_once(doc_id, store) so re-processing the same document id is a no-op. Runs offline.

Requirements:

  • Check whether doc_id is already recorded in the store before doing work
  • On a first sighting, record the id and return a processed result
  • On a repeat, return a skipped_duplicate result and do not write again
  • The store is keyed by id (a dict/set stands in for DynamoDB) so the guard is O(1)
  • Demonstrate the second call on the same id is skipped, offline

💡 Hint: The dedupe key is the document id; the presence check must happen before the write so a redelivery never repeats the effect.

Show solution

At-least-once delivery means duplicates; keying by id makes reprocessing safe.

def process_once(doc_id, store):
    if doc_id in store:
        return {"status": "skipped_duplicate", "id": doc_id}
    store[doc_id] = {"processed": True}      # idempotent write keyed by id
    return {"status": "processed", "id": doc_id}

seen = {}
print(process_once("doc-1", seen))   # processed
print(process_once("doc-1", seen))   # skipped_duplicate
Exercise 6 · A resilient invoice-ingest pipelineIndustry scenario

Context: Invoices land in S3 at unpredictable volume, and the pipeline must survive transient failures without ever posting an invoice twice. That means composing triggers, Step Functions retry/catch, and an idempotency guard into one hands-off flow.

Your task: Design the event-driven pipeline (S3 → Lambda → async Textract → SNS → Step Functions) and encode the failure policy: retry transient errors 3×, dead-letter after that, and never double-post. Model the decision offline.

Requirements:

  • The decision takes a document id, attempt number, and error state (plus the dedupe store)
  • Guard idempotency first: a known doc_id yields skip_duplicate
  • A transient error under the attempt cap yields retry (Step Functions Retry)
  • An error at/after the cap yields dead_letter (the Catch to a DLQ)
  • A clean run records the id and returns posted
  • Runs offline — it models the retry/idempotency decision, it doesn't call AWS

💡 Hint: Order the checks so the idempotency guard wins over retry, and retry wins over dead-letter; the attempt count versus a max is what separates "try again" from "give up".

Show solution

Compose triggers + Step Functions retry/catch + idempotency for a hands-off, resilient pipeline.

def handle(doc_id, attempt, error, store, max_attempts=3):
    if doc_id in store:
        return "skip_duplicate"               # idempotency guard
    if error and attempt < max_attempts:
        return "retry"                        # transient -> Step Functions Retry
    if error:
        return "dead_letter"                  # exhausted -> Catch to DLQ
    store[doc_id] = True
    return "posted"

store = {}
print(handle("A1", 1, "Throttling", store))   # retry
print(handle("A1", 3, "Throttling", store))   # dead_letter
print(handle("A1", 1, None, store))           # posted
print(handle("A1", 1, None, store))           # skip_duplicate

✓ Checkpoint — you can move on when you can…

  • Trigger a Lambda from an S3 upload.
  • Handle async Textract via SNS instead of polling.
  • Express a multi-step pipeline as a Step Functions state machine with retries + catch.
  • Explain why idempotency matters in event-driven AI.

Knowledge check check yourself

✓ Knowledge check

In the S3 -> Lambda -> Textract flow, why does the Lambda start_document_analysis and return immediately instead of waiting, and how is the result handled?

Show answer
Big-document Textract is async and can take a while, so the Lambda kicks off the job and returns fast (returning just the jobId). Textract posts to an SNS topic when finished, and a second Lambda listens on that topic to handle the result -- event-driven, not a hand-rolled polling loop.
✓ Knowledge check

In the Step Functions state machine, what is the difference between a state's Retry and its Catch, and why is idempotency stressed for event-driven pipelines?

Show answer
Retry automatically re-runs a failing state up to MaxAttempts; Catch routes any error to another state (e.g. a DeadLetter) instead of crashing the pipeline. Because events (and retries) can fire more than once, each step must be idempotent -- e.g. keying DynamoDB writes by document id -- so a replay never double-processes.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in