,

Tutorial: Deploy an Ambient Agent With Human Approval

Developer workstation deploying an ambient agent that reacts to an S3 invoice upload and waits for human approval

Today you will deploy an ambient agent on AWS that wakes up when a PDF lands in S3, waits for you to approve the job, runs, stops to ask you a question, and finishes with your answer. It uses the open-source aws-samples/sample-ambient-agent project, which wires S3, Lambda, SQS, DynamoDB and Amazon Bedrock AgentCore Runtime together with CDK. Budget about an hour, most of it waiting on Docker and CloudFormation.

Prerequisites

  • An AWS account and the AWS CLI configured with default Region us-east-1 (the sample’s default).
  • AWS CDK v2 installed, Python 3.11+, Node.js 18+.
  • Docker running locally. The agent ships as a container.
  • Model access to Anthropic Claude Sonnet 4.5 enabled in the Amazon Bedrock console. Skip this and your first job fails with an access error.
  • A sample PDF, such as a fake invoice. Please do not use a real one.

Steps to deploy an ambient agent with a human gate

Before typing anything, it helps to know what you are about to stand up. An S3 event notification calls a Signal Processor Lambda. That function looks up which signals care about the uploaded object and writes a job row to DynamoDB. Jobs that are approved, either automatically or by a person, go onto an SQS queue. A Job Execution Lambda drains the queue and calls the agent container in AgentCore Runtime, then writes the result and the conversation back to DynamoDB. A React UI sits on top so you can see jobs, approve them and answer the agent. Nothing runs when nothing is uploaded, so the sandbox costs pocket change while it idles.

  1. Clone the repo. Everything lives in three folders: backend (CDK), frontend (React) and agent (the container).

    git clone https://github.com/aws-samples/sample-ambient-agent.git
    cd sample-ambient-agent
    ls
    # agent/  backend/  frontend/ ...
    
  2. Bootstrap and deploy the backend. This creates the eight Lambdas, the DynamoDB tables, the SQS queue and DLQ, API Gateway with Cognito, the ECR repository and the hosting bucket. Save the outputs to a file; you will want the API URL, bucket names and table names. The first deploy takes a while because CloudFormation creates a few dozen resources. Go get coffee.

    cd backend
    pip install -r requirements.txt
    cdk bootstrap
    cdk deploy --all --outputs-file ../outputs.json
    cat ../outputs.json
    
  3. Build the frontend. The React app talks to API Gateway and signs in through Cognito. Follow the repo README for wiring the stack outputs into the frontend config and publishing the build to the CloudFront-backed bucket.

    cd ../frontend
    npm install
    npm run build
    
  4. Check the agent config and deploy the container. Open agent/config.yaml and confirm the human input tool is enabled. This is the ask_human tool that lets the agent pause. The same file sets the model ID and max_iterations: 10, which caps how many reasoning steps one run can take. Then run the deploy script, which builds the image, pushes it to Amazon ECR and registers it with AgentCore Runtime. Copy the Agent Runtime ARN it prints. If the push fails, it is almost always Docker not running or an expired ECR login.

    cd ../agent
    grep -A4 "human_input" config.yaml
    #  human_input:
    #    enabled: true
    #    type: "human_input"
    #    name: "ask_human"
    ./deploy_agent.sh
    # ... Agent Runtime ARN: arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/...
    
Agent container pushed to ECR and registered in AgentCore Runtime alongside a CDK deployment
  1. Register the agent. Sign in to the web UI (CloudFront URL from the outputs), go to the Agents page and add a new agent with the runtime ARN. The platform stores it in the agents table so signals and jobs can reference it by agentId. Confirm the runtime exists from the CLI first:

    aws bedrock-agentcore-control list-agent-runtimes \
      --region us-east-1 \
      --query "agentRuntimes[].[agentRuntimeName,status]" \
      --output table
    
  2. Create an S3 signal with autoExecute off. On the Signals page, create a signal for your documents bucket with prefix invoices/ and suffix .pdf, and leave Auto Execute unchecked. That forces every triggered job to wait for you. Under the hood the UI posts this body to /signals; you can send it yourself with an ID token copied from the browser session:

    curl -X POST "$API_URL/signals" \
      -H "Authorization: $ID_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "signalName": "Document Processor",
        "signalType": "s3_file_upload",
        "agentId": "agent-123abc",
        "autoExecute": false,
        "configuration": {
          "bucketName": "ambient-agent-documents",
          "prefix": "invoices/",
          "suffix": ".pdf"
        }
      }'
    
  3. Upload a file. Use the documents bucket name from outputs.json. The S3 event notification fires the Signal Processor Lambda, which matches your signal and writes a signal_triggered job.

    aws s3 cp ./invoice-0042.pdf s3://$DOCS_BUCKET/invoices/invoice-0042.pdf
    aws logs tail /aws/lambda/$SIGNAL_PROCESSOR_FN --since 5m
    
  4. Review the idle job and approve it. Open the Jobs page. Your job sits in idle status because autoExecute is false. Click Execute. The API enqueues it on SQS, returns immediately, and the Job Execution worker calls invoke_agent_runtime on AgentCore Runtime with a session ID tied to the job. If the logs show an access error here, revisit model access in the Bedrock console. Watch it go:

    aws logs tail /aws/lambda/$JOB_EXECUTION_FN --follow
    
  5. Answer the agent’s question. When the agent calls ask_human (for example, “Which expense category: Hardware, Services, or Licenses?”), the job flips to interrupted, gets a requiresAction flag and appears on the Interrupted tab. Confirm the state in DynamoDB, then click Provide Response in the UI and type your answer.

    aws dynamodb get-item \
      --table-name "$JOBS_TABLE" \
      --key '{"jobId": {"S": "<your-job-id>"}}' \
      --projection-expression "jobId, #s, requiresAction" \
      --expression-attribute-names '{"#s": "status"}'
    
  6. See the result. The worker re-invokes the agent on the same session with your answer folded into a continuation prompt. The job moves to completed, and the detail view shows the whole conversation, including your reply, for audit. Run the same get-item to confirm:

    aws dynamodb get-item \
      --table-name "$JOBS_TABLE" \
      --key '{"jobId": {"S": "<your-job-id>"}}' \
      --query "Item.status.S"
    # "completed"
    

What you built

A working ambient agent: an S3 upload becomes a signal, a signal becomes a job, a human approves it, the agent runs on AgentCore Runtime, pauses when it needs judgment, and resumes with full context. Flip Auto Execute on for the signal and the approval step disappears, while the mid-task question stays. Swap the agent’s tools in config.yaml and the same pipeline serves a completely different workflow.

The pattern is the valuable part, not the invoice demo. Any event source that can write a job row, such as a webhook or an EventBridge rule, plugs into the same queue, worker and approval flow without touching the agent.

Next experiments: set reserved concurrency on the worker, add an alarm on the DLQ, and try the scheduled job type. When you are done, tear it all down so nothing lingers:

aws bedrock-agentcore delete-agent-runtime --agent-runtime-arn <your-agent-runtime-arn>
aws s3 rm s3://$DOCS_BUCKET --recursive
aws s3 rm s3://$FRONTEND_BUCKET --recursive
cd backend && cdk destroy --all
aws ecr delete-repository --repository-name ambient-agent --force

At Enkompass (enkompass.net), this is our home turf. We build event-driven serverless systems that scale to zero and bill like it, and we take reference stacks like this one from sandbox demo to hardened production workload. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.

Ready when you are

Let’s map your route to AWS

A free 60-minute architecture review with a senior engineer. No slides, no SDR, just a plan.