An event-driven content preparation pipeline for long-form video production. Orchestrates five phases β research, outline, demo code, script draft, and script edit β through a Lambda Durable Function with human-in-the-loop review via Telegram.
graph TD
TG[Telegram] --> APIGW[API Gateway]
APIGW --> TBot[Telegram Bot<br/>Lambda]
TBot --> Orch[Orchestrator<br/>Lambda Durable Function]
Orch --> RA[Research Agent<br/>Lambda]
Orch --> OA[Outline Agent<br/>Lambda]
Orch --> DCA[Demo Code Agent<br/>Lambda]
Orch --> SDA[Script Draft Agent<br/>Lambda]
Orch --> SEA[Script Edit Agent<br/>Lambda]
RA --> BR[Amazon Bedrock]
OA --> BR
DCA --> BR
SDA --> BR
SEA --> BR
Orch --> BR
Orch --> TG
TBot --> DDB[(DynamoDB<br/>CallbackMappingTable)]
Orch --> DDB
Orch --> S3[(S3<br/>ArtifactBucket)]
All Lambdas run on ARM64 with Python 3.14 and share a Strands Agents layer. The orchestrator is a Lambda Durable Function β it sleeps at zero compute cost while waiting for agent callbacks and human approvals (up to 30 days).
sequenceDiagram
participant U as User (Telegram)
participant TB as Telegram Bot Lambda
participant O as Orchestrator (Durable)
participant A as Agent Lambda
U->>TB: Send topic message
TB->>O: Async invoke (Event)
TB->>U: "Starting pipeline..."
Note over O: classify_task_step
Note over O: create_pipeline_run_step
O->>U: Pipeline started
Note over O: wait_for_callback (approval-mode-choice)
O->>U: "Review Each Phase" or "Approve All" buttons
Note over O: π€ Sleeping (zero compute)
alt Review Each Phase
U->>TB: Tap π Review Each Phase
TB->>O: callback_success ({mode: review})
O->>U: "π Review mode enabled"
else Approve All
U->>TB: Tap β
Approve All
TB->>O: callback_success ({mode: auto})
O->>U: "β© Auto-approve enabled"
end
loop For each phase (research β outline β demo_code β script_draft β script_edit)
Note over O: wait_for_callback ({phase}/agent)
O->>A: Async invoke (Event) + callback_id
Note over O: π€ Sleeping (zero compute)
Note over A: Bedrock call + parse output
A->>O: send_durable_execution_callback_success (result)
Note over O: Resumes
alt Auto-approve mode
O->>U: Phase output (no buttons) + "β© auto-approved"
Note over O: Moves to next phase immediately
else Review mode
Note over O: wait_for_callback ({phase}/review)
O->>U: Phase output + Approve/Iterate/Go Back buttons
Note over O: π€ Sleeping (zero compute)
alt Approve
U->>TB: Tap β
Approve
TB->>O: callback_success ({action: approve})
O->>U: "β
Phase approved. Starting next..."
else Iterate
U->>TB: Tap π Iterate + send feedback
TB->>O: callback_success ({action: iterate, feedback})
O->>U: "π Revising with your feedback..."
Note over O: wait_for_callback ({phase}/agent-revision-N)
O->>A: Async invoke + feedback + callback_id
A->>O: callback_success (revised result)
Note over O: Back to review loop
else Go Back
U->>TB: Tap β¬
οΈ Go Back
TB->>O: callback_success ({action: go_back, target_phase})
Note over O: Jump to target phase
end
end
end
O->>U: "π All phases complete!"
The orchestrator never blocks waiting for agents. Each agent is invoked asynchronously and calls back the durable execution when done. Human review gates also use wait_for_callback with a 30-day timeout. The orchestrator consumes zero compute while waiting.
Before any phase runs, the user chooses an approval mode:
- Review Each Phase: approve/iterate/go-back after every phase (default interactive experience)
- Approve All: all phases run back-to-back with results sent to Telegram but no approval gates
Then for each phase (research β outline β demo_code β script_draft β script_edit):
- Orchestrator enters
wait_for_callbackand fires the agent Lambda async with the callback ID - Agent Lambda runs its Bedrock call, parses the result
- Agent Lambda calls
send_durable_execution_callback_successwith the output - Orchestrator resumes and uploads the artifact to S3
- If auto-approve: sends output to Telegram without buttons, moves to next phase
- If review mode: sends output with Approve / Iterate / Go Back buttons, waits for user
- Approve: stores output, notifies user, moves to next phase
- Iterate: notifies user, re-invokes agent with feedback
- Go Back: jumps to a previous phase
The orchestrator names every durable operation for traceability:
| Step Name | Type | Description |
|---|---|---|
classify_task_step |
@durable_step |
Classify user message via Haiku 4.5 |
create_pipeline_run_step |
@durable_step |
Create run ID and initial state |
send_telegram_step |
@durable_step |
All Telegram notifications |
send_telegram_info_step |
@durable_step |
Phase output without buttons (auto-approve) |
approval-mode-choice |
wait_for_callback |
Upfront review vs auto-approve choice |
{phase}/agent |
wait_for_callback |
Async agent invocation |
{phase}/agent-revision-{n} |
wait_for_callback |
Agent re-invocation after feedback |
{phase}/review |
wait_for_callback |
Human review gate (review mode only) |
{phase}/review-{n} |
wait_for_callback |
Review after nth revision |
- AWS CLI configured with credentials (via default profile, named profile, or environment variables)
- AWS SAM CLI (v1.153.1+)
- Python 3.14
- Bedrock cross-region inference profile access enabled for:
us.anthropic.claude-haiku-4-5-20251001-v1:0(task routing)us.anthropic.claude-sonnet-4-20250514-v1:0(outline, script draft)us.anthropic.claude-sonnet-4-5-20250929-v1:0(research, demo code, script edit)
- Open Telegram and search for @BotFather
- Send
/newbotand follow the prompts to name your bot - BotFather will give you a token β save this
Store the bot token:
aws secretsmanager create-secret \
--name content-prep-agent/telegram-bot-token \
--secret-string "YOUR_BOT_TOKEN"Generate and store a webhook secret. This is a shared secret between Telegram and your bot Lambda β Telegram sends it in the X-Telegram-Bot-Api-Secret-Token header on every webhook request, and the Lambda rejects anything that doesn't match. Pick any string you like (1β256 characters, alphanumeric and _-):
WEBHOOK_SECRET=$(openssl rand -hex 32)
aws secretsmanager create-secret \
--name content-prep-agent/telegram-webhook-secret \
--secret-string "$WEBHOOK_SECRET"sam build
sam deploy --guidedBOT_TOKEN=$(aws secretsmanager get-secret-value \
--secret-id content-prep-agent/telegram-bot-token \
--query SecretString --output text)
WEBHOOK_URL=$(aws cloudformation describe-stacks \
--stack-name content-prep-agent \
--query 'Stacks[0].Outputs[?OutputKey==`WebhookUrl`].OutputValue' \
--output text)
WEBHOOK_SECRET=$(aws secretsmanager get-secret-value \
--secret-id content-prep-agent/telegram-webhook-secret \
--query SecretString --output text)
curl -X POST "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook" \
-H "Content-Type: application/json" \
-d "{\"url\": \"${WEBHOOK_URL}\", \"secret_token\": \"${WEBHOOK_SECRET}\"}"The secret_token tells Telegram to include an X-Telegram-Bot-Api-Secret-Token header on every webhook request. The bot Lambda checks this header and rejects requests that don't match β so random HTTP traffic hitting your API Gateway endpoint gets a 403 instead of being processed.
The bot uses username-based authorization. Only users registered in the DynamoDB table can interact with the bot. To add a user:
TABLE_NAME=$(aws cloudformation describe-stacks \
--stack-name content-prep-agent \
--query 'Stacks[0].Outputs[?OutputKey==`CallbackMappingTableName`].OutputValue' \
--output text)
aws dynamodb put-item \
--table-name "$TABLE_NAME" \
--item '{"short_id": {"S": "user#yourtelegramusername"}}'Replace yourtelegramusername with the Telegram username (lowercase, no @ prefix).
To remove a user:
aws dynamodb delete-item \
--table-name "$TABLE_NAME" \
--key '{"short_id": {"S": "user#yourtelegramusername"}}'- Send a topic to your Telegram bot
- Choose approval mode: "Review Each Phase" for interactive control, or "Approve All" to let it run unattended
- If review mode: each phase shows results with Approve / Iterate / Go Back buttons
- If auto-approve: phases run back-to-back, results are sent to Telegram without buttons
- After the final phase, the pipeline completes
You can also reply directly to any phase output message with feedback text (review mode).
βββ template.yaml # SAM template
βββ samconfig.toml # SAM config
βββ layers/strands/ # Shared strands-agents layer
βββ src/
β βββ orchestrator/ # Durable function orchestrator
β β βββ app.py # Pipeline handler + durable steps
β β βββ task_router.py # Topic classification (Haiku 4.5)
β β βββ phase_runner.py # Async Lambda invocation
β β βββ telegram_notifier.py # Send outputs + callback buttons
β β βββ artifact_store.py # S3 artifact upload + presigned URLs
β β βββ models.py # Shared data models
β β βββ requirements.txt
β βββ telegram-bot/ # Webhook handler
β βββ research-agent/ # Lambda (Sonnet 4.5)
β βββ outline-agent/ # Lambda (Sonnet 4)
β βββ demo-code-agent/ # Lambda (Sonnet 4.5)
β βββ script-draft-agent/ # Lambda (Sonnet 4)
β βββ script-edit-agent/ # Lambda (Sonnet 4.5)
Configure your AWS profile and region in samconfig.toml.
Two secrets are resolved at deploy time from Secrets Manager:
content-prep-agent/telegram-bot-tokenβ the bot token from BotFathercontent-prep-agent/telegram-webhook-secretβ shared secret for webhook request validation
Bot doesn't respond: Check the webhook β curl https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo
Getting 403 Forbidden: The bot Lambda validates the X-Telegram-Bot-Api-Secret-Token header on every request. If you see 403s, make sure the secret_token you passed to setWebhook matches the value in Secrets Manager (content-prep-agent/telegram-webhook-secret).
Agent timeout: Agents have a 15-minute Lambda timeout and 10-minute Bedrock read timeout. If an agent fails, the orchestrator's wait_for_callback times out after 15 minutes and reports failure to Telegram.
Bedrock access denied: Enable model access in the Bedrock console for us-west-2.
Durable function issues: The orchestrator uses AutoPublishAlias: live. Durable functions require qualified ARNs β the template handles this automatically.