Automated Contract Extraction Pipeline

Wire five AWS AI services to turn a PDF contract into structured data.

Introduction

30 Second Summary

Contracts pile up in every organization. Legal teams spend hours reading through dense PDF documents to find a specific name, a date, or a clause buried in the fine print.

In this project, you will build an automated pipeline on AWS that turns unstructured contract PDFs into clean, queryable data. Upload a PDF, and the pipeline extracts the text, identifies the key entities inside it, and writes a structured summary to a database.

What You'll Build

You drop a contract PDF into an Amazon S3 bucket, and seconds later you pull up an Amazon DynamoDB record showing every person, organization, and date extracted from the text, complete with confidence scores and a sentiment breakdown.

By the end of this project, you'll have:

  • An event-driven pipeline where uploading a contract PDF to S3 triggers an AWS Lambda function that uses Amazon Textract to pull raw text from the document automatically.
  • Named entities like person names, organizations, and dates tagged by Amazon Comprehend with confidence scores, alongside a sentiment analysis of the overall contract language.
  • A structured JSON record in DynamoDB where you can compare the original PDF with the extracted data and see exactly what the AI found.
  • Secret Mission: Upgrade from plain text extraction to intelligent document processing by extracting key-value pairs from contract fields.

Do I need anything installed on my computer?

This project runs entirely in the AWS Management Console. No local tools, code editors, or terminal commands are needed. You just need an AWS account to get started.

Before We Start

Before you dive in, take a moment to commit to what you are about to build and why it matters. This is your chance to lock in the goal before any hands-on work begins.

Set Up Your S3 Bucket and Lambda Function

Your pipeline needs two things before it can process contracts: a place to receive PDF uploads, and a function that runs automatically when a new file arrives. Amazon S3 provides the storage bucket, and AWS Lambda provides the serverless compute.

Before these services can talk to each other, the Lambda function needs an IAM role with the right permissions to read from S3, call Textract and Comprehend, write to DynamoDB, and log to CloudWatch. In this step, you will create all three pieces and wire them together with an S3 trigger.

In this step, get ready to:
  • Create an S3 bucket to receive contract PDF uploads.
  • Create an IAM role with a custom permissions policy and a Lambda function.
  • Wire the S3 bucket to the Lambda function with an automatic trigger.
Create your S3 bucket

S3 bucket names must be globally unique across all AWS accounts. You will include your initials in the name to avoid collisions.

  • In the AWS Management Console, type S3 into the search bar at the top and select S3.
  • In the left navigation pane, click General purpose buckets.
  • Click Create bucket.
  • In the AWS Region selector at the top of the page, confirm you are in US East (N. Virginia) us-east-1. If not, switch to it now.
  • For Bucket name, enter contract-pipeline- followed by your initials, e.g. jd (for example, contract-pipeline-jd). Use only lowercase letters.
  • Leave all other settings at their defaults.
  • Scroll to the bottom and click Create bucket.

You should see your new bucket in the General purpose buckets list. The name you chose will appear with the region us-east-1 next to it.

Why include your initials in the bucket name?

S3 bucket names are globally unique across every AWS account in the world. Adding your initials prevents a naming conflict with someone else who has already created a bucket with the same name.

Seeing a 'Bucket name already exists' error?

Try adding more characters to make the name unique, such as contract-pipeline-jd-2026. Bucket names must be lowercase letters, numbers, and hyphens only.

Help me fix this bucket name error

Create the IAM role and Lambda function

An IAM role is like an identity card for your Lambda function. It tells AWS which services the function is allowed to talk to. You will create a role with a custom policy that grants exactly the permissions your pipeline needs.

  • In the console search bar, type IAM and select IAM.
  • In the left navigation pane, click Roles.
  • Click Create role.
  • For Trusted entity type, select AWS service.
  • For Service or use case, choose Lambda.
  • Click Next.
  • On the Add permissions page, do not select any managed policies. Click Next to skip this page.
  • For Role name, enter ContractProcessorRole.

Why skip the managed policies?

AWS offers pre-built managed policies, but none of them grant exactly the combination of permissions your pipeline needs. You will add a custom inline policy in the next few steps so your function gets only the access it requires.

  • Click Create role.

You should see a green success banner confirming the role was created.

  • In the Roles list, click the role name ContractProcessorRole to open it.
  • Select the Permissions tab.
  • Click Add permissions.
  • Select Create inline policy.
  • In the Policy editor, click the JSON tab.
  • Replace the default JSON in the editor by pasting the following policy:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::contract-pipeline-[[YOUR_INITIALS=\"your initials, e.g. jd\"]]/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "textract:DetectDocumentText",
        "textract:AnalyzeDocument",
        "comprehend:DetectEntities",
        "comprehend:DetectSentiment",
        "dynamodb:PutItem"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

What does this policy do?

This policy has three permission groups. The first grants s3:GetObject on your specific bucket so the function can read uploaded PDFs.

The second group grants access to Textract, Comprehend, and DynamoDB. These are the AI and storage services your pipeline will call in later steps.

The third group grants CloudWatch Logs permissions so your function can write log output. Without these, you would not be able to see any results.

  • Click Next.
  • For Policy name, enter ContractProcessorPolicy.
  • Click Create policy.

Back on the role's Permissions tab, you should now see ContractProcessorPolicy listed as an inline policy.

Policy not showing up?

Make sure you clicked Create inline policy (not Attach policies). Refresh the page and check the Permissions tab again.

Also confirm you replaced the bucket name placeholder in the policy JSON with your actual bucket name.

Help me troubleshoot my inline policy

Now create the Lambda function and attach this role to it.

  • In the console search bar, type Lambda and select Lambda.
  • Click Create function.
  • Select Author from scratch.
  • For Function name, enter ContractProcessor.
  • For Runtime, select Python 3.13.
  • Expand the Change default execution role section.
  • Select Use an existing role.
  • In the Existing role dropdown, choose ContractProcessorRole.

What is an execution role?

Every Lambda function needs an execution role that defines what AWS services it can access. By selecting the role you just created, your function inherits the exact permissions you defined in the inline policy.

  • Click Create function.

Lambda creates your function and opens the function editor. You should see the Function overview at the top of the page.

  • Click the Configuration tab below the function editor.
  • In the left sidebar, click General configuration.
  • Click Edit.
  • Set Memory to 256 MB.
  • Set Timeout to 1 minute and 0 seconds.
  • Click Save.

Why 256 MB and 60 seconds?

The default Lambda timeout is 3 seconds, which is far too short for Textract to process a PDF. Setting the timeout to 60 seconds gives the function enough time to call Textract, Comprehend, and DynamoDB.

256 MB of memory provides enough CPU for JSON processing. The Textract and Comprehend calls are network-bound (waiting for API responses), so you do not need more compute power than this.

Add the S3 trigger

The Lambda function exists, but nothing tells it to run when a PDF arrives in S3. An S3 trigger connects the two so that every new .pdf upload automatically invokes your function.

  • In the ContractProcessor function page, scroll up to the Function overview section.
  • Click Add trigger.
  • In the trigger source dropdown, select S3.
  • For Bucket, select your contract-pipeline-your initials, e.g. jd bucket.
  • For Event types, select All object create events.
  • For Suffix, enter .pdf.

Why add a suffix filter?

The .pdf suffix filter ensures the Lambda function only triggers when a PDF file is uploaded. Without it, uploading any file type (images, text files, folders) would invoke the function unnecessarily.

You will see a warning about recursive invocations. This warns that if your Lambda writes back to the same bucket that triggers it, it could create an infinite loop. Your function does not write back to S3, so this does not apply.

  • Check the acknowledgment box to confirm you understand the warning.
  • Click Add.

Before you check the result, what do you think you will see in the Function overview now that the trigger has been added?

Look at the Function overview section at the top of the page. You should see an S3 trigger icon on the left side, connected to your ContractProcessor function. The trigger shows your bucket name underneath it.

Confirm the runtime displays Python 3.13 in the function details below the overview. Your S3 bucket, IAM role, Lambda function, and trigger are all wired up.

Don't see the S3 trigger in the overview?

Refresh the page and check the Function overview section again. The trigger can take a few seconds to appear.

If the trigger is missing entirely, make sure you selected the correct bucket and clicked Add (not Cancel).

Help me fix my S3 trigger

Your infrastructure is ready. Next up, you will write the Lambda function code that uses Textract to extract text from a contract PDF and see your first result in CloudWatch Logs.

Extract Text with Textract and See Your First Result

In the previous step, you wired up the infrastructure: an S3 bucket to receive PDFs, a Lambda function to process them, and an IAM role that ties everything together.

The pipeline is wired but does nothing yet. Adding Amazon Textract text extraction is the fastest way to prove the entire chain works: upload a PDF, Lambda fires, Textract reads the document, and you see extracted contract text in CloudWatch Logs.

In this step, get ready to:
  • Write the contract processor function in the Lambda inline code editor.
  • Upload a sample PDF contract to S3 and confirm Textract extracts the text.
Write the contract processor function

Your Lambda function currently has default placeholder code. You need to replace it with code that reads the incoming S3 event, sends the uploaded PDF to Textract for text extraction, and prints the result.

  • Return to the ContractProcessor function in the Lambda console.
  • Click the Code tab to open the inline code editor.
  • Select all the existing code in lambda_function.py and delete it.
  • Paste the following imports and client setup code:
import json
import urllib.parse
import boto3
from datetime import datetime, timezone

# Create AWS service clients
textract = boto3.client('textract')
comprehend = boto3.client('comprehend')
dynamodb = boto3.client('dynamodb')

# DynamoDB table name for storing results
TABLE_NAME = 'ContractResults'

What does this code do?

This section sets up the AWS service clients your function needs. The Lambda Python runtime includes boto3 (the AWS SDK for Python) by default, so no extra packages are needed.

  • textract, comprehend, and dynamodb are service clients. You will use Textract in this step and add the others in later steps.
  • TABLE_NAME stores the DynamoDB table name you will create in a later step.
  • Directly below the code you just pasted, add the handler function that reads the S3 event and calls Textract:
def lambda_handler(event, context):
    # Get the bucket and file name from the S3 event
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(
        event['Records'][0]['s3']['object']['key'], encoding='utf-8'
    )
    print(f"Processing document: s3://{bucket}/{key}")

    # Step 2: Extract text with Textract
    textract_response = textract.detect_document_text(
        Document={'S3Object': {'Bucket': bucket, 'Name': key}}
    )

    # Join all LINE-type blocks into a single text string
    extracted_text = ''
    for block in textract_response['Blocks']:
        if block['BlockType'] == 'LINE':
            extracted_text += block['Text'] + '\n'

    print(f"Extracted {len(extracted_text)} characters of text")
    print(f"First 500 chars:\n{extracted_text[:500]}")

What does this code do?

  • bucket and key are pulled from the S3 event that triggered the function. The key is URL-decoded because S3 encodes special characters in object names.
  • detect_document_text() sends the PDF to Textract, which reads every line of text from the document and returns it as a list of Blocks.
  • The loop filters for blocks with BlockType of LINE and joins them into one string. Textract also returns WORD and PAGE blocks, but LINE gives you readable sentences.
  • The print() statements write to CloudWatch Logs so you can verify the extraction worked.
  • Click the Deploy button above the code editor to save your function.

You should see a "Successfully updated the function ContractProcessor" banner at the top of the page.

Not seeing the success banner?

  • Check for any red error highlights in the code editor. A missing colon, extra indent, or mismatched quote will prevent deployment.
  • Make sure the handler function is named exactly lambda_handler. This matches the default handler setting in the Lambda configuration.

Still stuck? Help me fix a deployment error in my Lambda function.

✔️ Awesome, I've got everything!

Great. Double check that you clicked Deploy and saw the success banner before moving on.

ⓧ I'd like to double check the full code

Here is the complete lambda_function.py at this point. Make sure your code matches this exactly:

import json
import urllib.parse
import boto3
from datetime import datetime, timezone

textract = boto3.client('textract')
comprehend = boto3.client('comprehend')
dynamodb = boto3.client('dynamodb')

TABLE_NAME = 'ContractResults'

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(
        event['Records'][0]['s3']['object']['key'], encoding='utf-8'
    )
    print(f"Processing document: s3://{bucket}/{key}")

    # Step 2: Extract text with Textract
    textract_response = textract.detect_document_text(
        Document={'S3Object': {'Bucket': bucket, 'Name': key}}
    )

    extracted_text = ''
    for block in textract_response['Blocks']:
        if block['BlockType'] == 'LINE':
            extracted_text += block['Text'] + '\n'

    print(f"Extracted {len(extracted_text)} characters of text")
    print(f"First 500 chars:\n{extracted_text[:500]}")
Upload a PDF and verify in CloudWatch Logs

Your function is deployed. Now you need a sample contract PDF to test the pipeline end-to-end.

Getting a sample contract PDF

Search the web for "sample contract PDF" or "sample agreement PDF" to find a free one-page contract. A simple agreement with party names, dates, and terms works best.

You can also create your own: type a short agreement in a document editor (Google Docs, Word, or Pages) and export it as a PDF. Include names, dates, and a few paragraphs of terms so Textract has meaningful text to extract.

  • Open the S3 console and click on your contract-pipeline-your initials bucket.
  • Click Upload.
  • Click Add files and select your sample contract PDF.
  • Click Upload at the bottom of the page.

The upload triggers your Lambda function automatically through the S3 event trigger you configured in Step 1.

Before you check the logs, think about what you expect to see. The function calls Textract on a PDF and prints the result. What do you think CloudWatch Logs will show?

  • Return to your ContractProcessor function in the Lambda console.
  • Click the Monitor tab.
  • Click View CloudWatch logs. This opens the CloudWatch console showing your function's log group.
  • Click on the most recent log stream (the one at the top of the list).

What should I see?

You should see log entries including a "Processing document" line with your file name, an "Extracted X characters of text" line, and the first 500 characters of the extracted contract text. The text matches what is in your PDF, pulled out by Textract line by line.

Don't see any log streams?

  • Logs can take up to 5 minutes to appear. Wait a moment and refresh the page.
  • Make sure your uploaded file has a .pdf extension. The S3 trigger is filtered to only fire on .pdf files.
  • Confirm your S3 bucket and Lambda function are in the same AWS region. Textract's synchronous API requires the S3 object to be in the same region.
  • If you see an "AccessDenied" error in the logs, double check that your IAM role has s3:GetObject and textract:DetectDocumentText permissions.

Need more help? Help me troubleshoot why my Lambda function did not trigger after uploading a PDF to S3.

The extracted text is there. Textract successfully read every line from your contract PDF. But look at it closely.

It is a wall of unstructured characters. You cannot tell who the contract parties are, what dates are referenced, or whether the contract language is favorable or risky.

Why is raw text not enough?

Textract solved the first problem: getting text out of a PDF. But for a legal-tech workflow, you need to know what is in the text. Which words are party names? Which are dates? Is the overall language positive, neutral, or adversarial?

That is the gap you will close in the next step with Amazon Comprehend.

You have proven the pipeline works end-to-end. A PDF goes into S3, Lambda fires, and Textract pulls text out. Next up, you will turn that wall of text into structured intelligence by tagging entities and analyzing sentiment with Amazon Comprehend.

Analyze the Text with Amazon Comprehend

In the last step, you proved the pipeline works end to end. Textract pulled text out of your PDF and printed it to CloudWatch Logs.

But scroll through that output and you will see the problem. The extracted text is a blob of characters with no structure. You cannot tell who the contract parties are, what dates matter, or whether the language is favorable.

Amazon Comprehend solves this by tagging named entities and scoring the overall sentiment, turning the text wall into actionable intelligence.

In this step, get ready to:
  • Add Comprehend entity detection and sentiment analysis to the Lambda function.
  • Upload the PDF again and see tagged entities and sentiment scores in CloudWatch Logs.
Add Comprehend analysis to your Lambda function

Right now, the Lambda function extracts text and prints it. You need to send that text to Comprehend for two analyses: entity detection (to find names, dates, and organizations) and sentiment analysis (to gauge the tone of the contract language).

  • Return to the ContractProcessor function in the Lambda console.
  • Click the Code tab to open the inline code editor.
  • In lambda_function.py, find the last line of your existing code:
    print(f"First 500 chars:\n{extracted_text[:500]}")
  • Add the following Comprehend analysis code directly below that line:
    # Step 3: Analyze with Comprehend (truncate to 4900 chars for 5KB limit)
    analysis_text = extracted_text[:4900]

    entities_response = comprehend.detect_entities(
        Text=analysis_text, LanguageCode='en'
    )
    sentiment_response = comprehend.detect_sentiment(
        Text=analysis_text, LanguageCode='en'
    )

    entities = entities_response['Entities']
    sentiment = sentiment_response['Sentiment']
    sentiment_scores = sentiment_response['SentimentScore']

    print(f"Detected {len(entities)} entities")
    for entity in entities[:10]:
        print(f"  {entity['Type']}: {entity['Text']} ({entity['Score']:.2f})")

    print(f"Sentiment: {sentiment}")
    print(f"Scores: {json.dumps(sentiment_scores, default=str)}")

What does this code do?

The comprehend client was already created at the top of your file. These two new calls send the extracted text to Comprehend for analysis.

  • detect_entities scans the text and returns a list of entities it recognizes. Each entity has a Type (like PERSON, ORGANIZATION, or DATE), the matched Text, and a confidence Score.
  • detect_sentiment returns an overall label (POSITIVE, NEGATIVE, NEUTRAL, or MIXED) and a breakdown of scores for each category.
  • The text is truncated to 4,900 characters because Comprehend's detect_sentiment API has a 5 KB per-request limit for synchronous single-document operations.
  • Click Deploy to publish your updated function code.

Don't see the Deploy button?

Make sure you are on the Code tab and that you have lambda_function.py selected in the file explorer on the left side of the editor.

Help me find the Deploy button in the Lambda console.

✔️ Awesome, I've got everything!

Great. Make sure your function shows "Changes deployed" before moving on.

ⓧ I'd like to double check the full code

Your lambda_function.py should contain these sections in order:

  • Imports (json, urllib.parse, boto3, datetime) and three client initializations (textract, comprehend, dynamodb).
  • TABLE_NAME = 'ContractResults' constant.
  • The lambda_handler function reading the S3 event (bucket name and object key).
  • The Textract extraction block calling detect_document_text and joining LINE blocks.
  • The Comprehend analysis block you just added: detect_entities and detect_sentiment calls, followed by the entity and sentiment print statements.

Scroll up and confirm each section is present and in order.

Upload the PDF and verify entity detection

The code is deployed. Time to trigger the pipeline again and see if Comprehend can make sense of the text that Textract extracted.

  • Switch back to the S3 console and open your contract-pipeline-your initials bucket.
  • Upload the same sample PDF contract you used in the previous step.

Before you check CloudWatch, take a moment to predict: what entity types do you expect Comprehend to find in a contract document?

  • Return to the Lambda console and click the Monitor tab on the ContractProcessor function.
  • Click View CloudWatch logs to open the CloudWatch console.
  • Select the most recent Log stream at the top of the list.

You should see new lines in the log output showing detected entities with their types and confidence scores. Look for entries like PERSON (party names), ORGANIZATION (company names), and DATE (effective dates, termination dates).

Below the entities, you should also see a sentiment label (such as NEUTRAL or MIXED) with a score breakdown.

Why is contract sentiment usually NEUTRAL or MIXED?

Legal language is designed to be precise and impartial. Comprehend typically scores contracts as NEUTRAL because the text avoids emotional or persuasive language.

You might see MIXED if the contract includes clauses about penalties or obligations.

Not seeing entities or sentiment in the logs?

  • Make sure you clicked Deploy after adding the Comprehend code. Undeployed changes will not run.
  • Check that the new code is indented inside the lambda_handler function. Code outside the function body will not execute on each invocation.
  • Confirm the IAM role attached to your function includes comprehend:DetectEntities and comprehend:DetectSentiment permissions.

Help me debug why Comprehend entities and sentiment are not appearing in my CloudWatch logs.

The pipeline now extracts text AND understands what is in it. But there is a catch. These results only exist in CloudWatch Logs. When the Lambda execution ends, there is no persistent, queryable record of what was found.

Your pipeline can now read a PDF and understand what is inside it. Next up, you will store these structured results in a DynamoDB table so they persist beyond the Lambda execution and become queryable.

Store Structured Results in DynamoDB

Your pipeline now extracts text with Textract and analyzes it with Comprehend. You can see tagged entities and sentiment scores in CloudWatch Logs.

The problem is that CloudWatch Logs are not designed for structured queries or long-term access. Writing a structured record to DynamoDB makes your extraction results persistent, queryable, and ready for a downstream dashboard or API. This step completes the "before PDF / after structured JSON" comparison.

In this step, get ready to:
  • Create a DynamoDB table to store extraction results.
  • Update the Lambda function to write structured records after each extraction.
  • Upload a PDF and verify the structured record appears in DynamoDB.
Create the ContractResults DynamoDB table

DynamoDB is a fully managed NoSQL database. Each record (called an item) is identified by a partition key. Your table uses the uploaded document's filename as its partition key, so each PDF gets its own record.

  • In the AWS Management Console search bar, type DynamoDB and select DynamoDB from the results.
  • Click Create table.
  • In the Table name field, enter ContractResults.
  • In the Partition key field, enter DocumentId and leave the type set to String.
  • Leave all other settings at their defaults.
  • Scroll down and click Create table.

Why use defaults?

The default provisioned capacity of 5 read capacity units and 5 write capacity units falls within the DynamoDB free tier. For a single test document, this is more than enough.

  • Wait for the table status to change to Active. This usually takes a few seconds.

Table stuck in Creating status?

Refresh the page. If the status still shows Creating after a minute, check that you did not accidentally add a sort key or enable DynamoDB Streams, which can slow creation.

Still stuck? Help me figure out why my DynamoDB table is not becoming Active.

Add the DynamoDB write to the Lambda function

DynamoDB's low-level API requires you to specify the data type for every value. Strings use {'S': value}, numbers use {'N': value}, lists use {'L': [...]}, and maps use {'M': {...}}. The next two code chunks build a properly typed item and write it to your table.

  • Return to the ContractProcessor Lambda function in the AWS console.
  • In the Code tab, scroll to the bottom of your lambda_function.py code.
  • Find this line:
    print(f"Scores: {json.dumps(sentiment_scores, default=str)}")
  • Add the following code below it to build a list of the top 10 entities in DynamoDB's typed format:
    # Step 4: Store results in DynamoDB
    top_entities = []
    for entity in entities[:10]:
        top_entities.append({
            'M': {
                'Type': {'S': entity['Type']},
                'Text': {'S': entity['Text']},
                'Score': {'N': f"{entity['Score']:.4f}"}
            }
        })

What does this code do?

  • entities[:10] takes the first 10 entities that Comprehend detected. Contracts can produce dozens of entities, but the top 10 captures the most relevant ones.
  • Each entity is wrapped in DynamoDB's typed format: 'M' means "map" (an object), 'S' means "string", and 'N' means "number".
  • Directly below the entities loop you just added, paste the following code to write the full record to DynamoDB and return a response:
    dynamodb.put_item(
        TableName=TABLE_NAME,
        Item={
            'DocumentId': {'S': key},
            'Timestamp': {'S': datetime.now(timezone.utc).isoformat()},
            'ExtractedText': {'S': extracted_text[:2000]},
            'Entities': {'L': top_entities},
            'Sentiment': {'S': sentiment},
            'SentimentScores': {
                'M': {
                    'Positive': {'N': f"{sentiment_scores['Positive']:.4f}"},
                    'Negative': {'N': f"{sentiment_scores['Negative']:.4f}"},
                    'Neutral': {'N': f"{sentiment_scores['Neutral']:.4f}"},
                    'Mixed': {'N': f"{sentiment_scores['Mixed']:.4f}"}
                }
            }
        }
    )

    print(f"Results written to DynamoDB table {TABLE_NAME}")

    return {
        'statusCode': 200,
        'body': json.dumps({
            'document': key,
            'entities_found': len(entities),
            'sentiment': sentiment
        })
    }

What does this code do?

  • dynamodb.put_item() writes a single item to your ContractResults table. The DocumentId is the S3 object key (the PDF filename), so each document gets its own record.
  • ExtractedText stores the first 2,000 characters of the extracted text. This keeps the record size manageable while preserving enough text for reference.
  • SentimentScores is a map containing all four sentiment dimensions: Positive, Negative, Neutral, and Mixed. Together they sum to 1.0.
  • The return statement sends a summary response back to the caller, confirming the document was processed.
  • Click Deploy to save and deploy your updated function.
  • You should see a green banner confirming the function was updated successfully.

Seeing a deployment error?

Check for mismatched brackets or parentheses. The put_item call has deeply nested curly braces. Make sure every opening { has a matching closing }.

Also confirm that the new code is indented inside the lambda_handler function (one level of indentation).

Still stuck? Help me debug my Lambda deployment error.

✔️ Awesome, I've got everything!

Great. Double check you clicked Deploy and saw the green success banner.

ⓧ I'd like to double check the full code

Your lambda_function.py is now longer than 30 lines. Scroll through your code in the inline editor and confirm each section is present, in order:

  • Imports and client setup at the top (json, urllib.parse, boto3, datetime), three clients (textract, comprehend, dynamodb), and TABLE_NAME.
  • The lambda_handler function: S3 event parsing, Textract detect_document_text call, and text extraction loop.
  • Comprehend calls: detect_entities and detect_sentiment on the truncated text, with print statements for entities and sentiment.
  • DynamoDB write: top_entities list builder, dynamodb.put_item() call, print confirmation, and return statement.
Upload the PDF and verify the structured record

The entire pipeline is now wired. Time to trigger it one final time and see the "before PDF / after structured JSON" comparison.

  • Switch back to your S3 bucket in the AWS console.
  • Click Upload, then Add files.
  • Select the same sample PDF contract you uploaded before.
  • Click Upload.

Why upload the same file?

Uploading the same file overwrites the existing S3 object, which still counts as a new "object create" event. This triggers your Lambda function again, and since the DocumentId is the filename, the DynamoDB record will be created (or updated if it already exists).

Before you check DynamoDB, take a moment to predict what you expect to see. What fields should the record contain? What format do you think the entities will be in?

  • In the AWS console search bar, type DynamoDB and select DynamoDB.
  • Click Tables in the left sidebar, then click ContractResults.
  • Click Explore table items.

You should see a new item in the table. Click on it to expand the full record. Your structured record contains the document filename, a timestamp, the first 2,000 characters of extracted text, up to 10 tagged entities with their types and confidence scores, the overall sentiment label, and a breakdown of all four sentiment scores.

Compare it with the original PDF sitting on your desktop. Parties, dates, organizations, and sentiment are all extracted and structured. That is the "before PDF / after structured JSON" comparison your pipeline delivers.

No item in the table?

  • Check CloudWatch Logs for the ContractProcessor function. Look for error messages. A common cause is a mismatch between the table name in your code (ContractResults) and the table you created in the DynamoDB console.
  • Verify the IAM role attached to your Lambda function includes dynamodb:PutItem permission. Without it, the write silently fails.
  • Make sure you clicked Deploy after pasting the new code. Unsaved changes in the inline editor do not take effect.

Need a hand? Help me figure out why my DynamoDB item did not appear.

Your pipeline is complete. A PDF goes into S3, and seconds later a structured, queryable record appears in DynamoDB. Next up, take on the Secret Mission and upgrade from plain text extraction to intelligent document processing with key-value pair extraction.

Secret mission

Extract Key-Value Pairs with AnalyzeDocument

Your pipeline extracts raw text and tags entities, but contracts have structured form fields like "Effective Date:" and "Party A:" that get lost in the text blob. Can you upgrade from basic OCR to intelligent document processing by switching to Textract AnalyzeDocument with FORMS?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project's pipeline is event-driven, so your resources cost nothing while idle. Lambda only runs when you upload a PDF to S3.

Resources you used:

  • S3 bucket (contract-pipeline-{initials}) with uploaded PDF contracts.
  • Lambda function (ContractProcessor) with S3 trigger and Python 3.13 runtime.
  • DynamoDB table (ContractResults) with structured extraction records.
  • IAM role with inline policy for S3, Textract, Comprehend, DynamoDB, and CloudWatch Logs permissions.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the pipeline.

  • Lambda is event-driven. It only runs (and incurs compute charges) when you upload a PDF to S3. No uploads means zero Lambda cost.
  • DynamoDB is always free within 25 RCU, 25 WCU, and 25 GB of storage.
  • S3 storage for a few test PDFs is negligible.
  • The Textract free trial lasts 3 months (1,000 pages/month for DetectDocumentText). The Comprehend free tier lasts 12 months (50,000 units/month per API). After these windows expire, each future PDF upload costs under $0.01 to process.

Pause - I'll come back to this later

No action needed. The pipeline is completely idle unless you upload a new PDF to S3.

  • Lambda does not run on a schedule. It only fires when an S3 upload triggers it.
  • DynamoDB and S3 storage costs for a handful of test documents are effectively zero.
  • When you are ready to resume, upload a new PDF and the pipeline picks up right where you left off.

Delete - I don't want to use this again

Remove all project resources to start fresh. Follow these steps in order.

Delete the S3 bucket:

  • Open the S3 console and choose General purpose buckets in the left navigation pane.
  • Select the option button next to your contract-pipeline-{initials} bucket and choose Empty.
  • Type permanently delete to confirm, then choose Empty.
  • Return to the buckets list, select the same bucket, and choose Delete.
  • Type the bucket name to confirm, then choose Delete bucket.

Delete the Lambda function:

  • Open the Lambda console and go to the Functions page.
  • Select the ContractProcessor function.
  • Choose Actions, then Delete.
  • Type confirm in the text input field and choose Delete.

Delete the DynamoDB table:

  • Open the DynamoDB console and choose Tables in the navigation pane.
  • Select the ContractResults table and choose Delete.
  • Confirm the deletion when prompted.

Delete the IAM role:

  • Open the IAM console and choose Roles in the navigation pane.
  • Select the check box next to the role you created for this project.
  • Choose Delete at the top of the page.
  • Enter the role name to confirm, then choose Delete.

(Optional) Delete the CloudWatch log group:

  • Open the CloudWatch console and choose Log groups in the left navigation pane.
  • Select the log group named /aws/lambda/ContractProcessor.
  • Choose Actions, then Delete log group(s).
  • Choose Delete in the confirmation dialog.

Nice Work!

Nice Work!

You just built a fully automated, serverless document intelligence pipeline. A PDF contract goes into an S3 bucket, and structured, queryable data comes out the other side in DynamoDB.

You've learned how to:

  • Built an event-driven pipeline where uploading a PDF to S3 automatically triggers a Lambda function that extracts text, analyzes entities and sentiment, and writes structured results to a database.
  • Hit the "wall of unstructured text" problem and solved it with Amazon Comprehend's named entity recognition and sentiment analysis, turning raw characters into tagged PERSON, ORGANIZATION, and DATE entities with confidence scores.
  • Wired five AWS services together (S3, Lambda, Textract, Comprehend, DynamoDB) with a properly scoped IAM role, completing the "before PDF / after structured JSON" comparison in the DynamoDB console.
  • Secret Mission: Upgraded from basic OCR to intelligent document processing by switching to Textract AnalyzeDocument with FORMS to extract key-value pairs from contract fields.

Ready to quiz yourself?