AWS Builder Center

Automating Operational Workflows with AWS DevOps Agent Custom Agents, Amazon EventBridge, and AWS Lambda

Custom agents give DevOps Agent ability to run recurring operational tasks per schedule. Combined with EventBridge and Lambda, every agent output becomes a trigger for automated action, turning scheduled scans into closed-loop operational workflows.

What Are Custom Agents?

AWS DevOps Agent's built-in capabilities handle reactive operations — when a CloudWatch alarm fires, the agent investigates automatically. Custom agents gives Devops Agent capability run recurring operational tasks per schedule.
Think of a custom agent as an AI-powered cron job with access to your AWS environment, documentation, MCP tools, and the ability to produce structured reports and recommendations. You describe what to do in a system prompt using natural language, assign the tools it needs, and set a schedule. The agent handles the rest.
Custom agents complement — not replace — the built-in incident investigation capability. Built-in: reactive. Custom agents: proactive.

Architecture Overview

Before diving into the details, here is the end-to-end flow. A custom agent runs on a schedule or on-demand, and every invocation automatically flows through EventBridge into Lambda, which fetches the output and takes action.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Custom Agent (scheduled: rate(1 day) or on-demand)
│
│ CreateBacklogTask (CloudTrail event)
▼
Amazon EventBridge
(filter: source=aws.aidevops, eventName=CreateBacklogTask, assetId + agentSpaceId)
│
▼
AWS Lambda
│ polls GetBacklogTask every 30s until COMPLETED
│ fetches artifact via DevOps Agent Data Plane API
│ formats and sends output
▼
Actions:
├── SNS → Email report
├── Jira API → Create tickets per finding
├── ACM API → Renew expiring certificates
├── Step Functions → Drift remediation workflow
└── Slack webhook → Team notification
The sections below walk through each step of this flow.

Example Custom Agents

Each example below includes the system prompt to paste into the DevOps Agent Agents page when creating the agent, and add triggers to schedule.
EOS Support Scanner - runs weekly, scans Lambda runtimes across accounts and regions, identifies functions on deprecated or near-deprecated Python/Node.js runtimes, and produces a severity-tagged artifact report.
Create an agent with this prompt:
1
2
Scan all Lambda functions across all associated AWS accounts and regions. Identify functions
running deprecated or near-deprecated runtimes and produce an End-of-Support Report artifact.

Certificate Expiry Monitor - runs bi-weekly, checks ACM certificates expiring within 30/60/90 days, creates a recommendation per certificate, and feeds into renewal workflows.
Create an agent with this prompt:
1
2
Check all ACM certificates across all associated AWS accounts and regions. For any certificate
expiring within 90 days, create a recommendation and produce a Certificate Expiry Report artifact.

Security Posture Scanner - runs nightly, checks S3 public ACLs, security groups open to 0.0.0.0/0, and unencrypted EBS volumes, creating one finding per violation.
Create an agent with this prompt:
1
2
Scan all associated accounts for S3 public ACLs, security groups open to 0.0.0.0/0 on sensitive
ports, and unencrypted EBS volumes. Produce a Security Posture Report artifact with findings.

Understanding the Custom Agent Asset ID

Every custom agent has a stable unique identifier called an Asset ID (ki-* format). This ID is assigned when the agent is created and never changes. It is the key that lets you filter EventBridge events to a specific agent.
The assetId and agentSpaceId are stable and permanent — they identify the agent and its space. The taskId is generated fresh for each invocation and is what Lambda uses to poll for completion.
How to find your agent's Asset ID:
Method 1 — From the DevOps Agent web app URL (easiest)
Navigate to your agent space web app, go to Agents, and click on the custom agent. The Asset ID is the last segment of the URL:
1
https://\<agent-space-id\>.aidevops.global.app.aws/agents/\<asset-id\>
For example:
1
https://34db9656-5da2-4874-8b76-3d1010542707.aidevops.global.app.aws/agents/ki-f31f1790-cf75-4032-acab-6852c77cf7ef
In this example, ki-f31f1790-cf75-4032-acab-6852c77cf7ef is the Asset ID.
Method 2 — From AWS CloudTrail after the first invocation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
aws cloudtrail lookup-events \
--region us-east-1 \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreateBacklogTask \
--max-results 10 \
--query "Events[*].CloudTrailEvent" --output text \
| python3 -c "
import sys, json
for line in sys.stdin:
try:
e = json.loads(line)
req = e.get('requestParameters', {})
resp = e.get('responseElements', {}).get('task', {})
print(f\"Asset ID: {req.get('assetId')} Task ID: {resp.get('taskId')} Time: {e.get('eventTime')}\")
except: pass
"

How Custom Agent Invocations Flow Through EventBridge

When a custom agent runs, DevOps Agent calls CreateBacklogTask internally. This API call is recorded by AWS CloudTrail and delivered to Amazon EventBridge as a management event.

EventBridge Rule: Filter to a Specific Custom Agent

Filter on assetId to ensure your rule only triggers for your specific agent. The assetId is unique per custom agent, so this alone is sufficient:
1
2
3
4
5
6
7
8
9
10
{
"source": ["aws.aidevops"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["CreateBacklogTask"],
"requestParameters": {
"assetId": ["ki-a4a115b4-c486-46b9-974d-ce3c7cc7f512"]
}
}
}
If you want extra hardening — for example, if you have multiple agent spaces and want to be explicit — you can also add agentSpaceId:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"source": ["aws.aidevops"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["CreateBacklogTask"],
"requestParameters": {
"assetId": ["ki-a4a115b4-c486-46b9-974d-ce3c7cc7f512"]
},
"responseElements": {
"task": {
"agentSpaceId": ["34db9656-5da2-4874-8b76-3d1010542707"]
}
}
}
}
> Important: Custom agent invocations do not emit a lifecycle completion event to EventBridge. This has been confirmed via CloudTrail — only CreateBacklogTask (the start event) is recorded. Built-in investigations emit Investigation Completed, but custom agents do not. The CreateBacklogTask CloudTrail event is your only integration point. Lambda must poll GetBacklogTask to detect completion.

Fetching the Agent's Output

Custom agents produce two types of output you can retrieve programmatically:
1. Artifacts — structured visual reports (tables, charts, topology diagrams) produced by the agent and stored persistently in the agent space. This is the cleanest output format — a single JSON document containing all the structured data the agent generated.
2. Journal records — the full conversation log of the invocation, accessible via ListJournalRecords. Records are of type message and contain the agent's reasoning, tool calls, and final text response.

Retrieving Artifacts via the Data Plane API

Custom agent artifacts are stored in the DevOps Agent Data Plane and accessible via direct HTTP calls. The boto3 SDK does not yet include the asset APIs (ListAssets, GetAssetContent) — they exist in the API reference  and Data API documentation  but have not yet been added to the SDK service model. Until they are, you can call the Data Plane API directly using SigV4-signed HTTP requests.
The Data Plane endpoint is https://dp.aidevops.{region}.api.aws — note the dp. prefix (data plane), distinct from cp. (control plane) used by the regular SDK operations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import boto3, json, base64, zipfile, io, urllib.request
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

def get_asset_content(agent_space_id, asset_id, region='us-east-1'):
"""
Fetch artifact content via the DevOps Agent Data Plane API.
Returns parsed artifact dict containing 'elements' (text, tables, charts).

Endpoint: GET https://dp.aidevops.{region}.api.aws/asset/agent-space/{spaceId}/assets/{assetId}/content
Response: {"content": {"zipFile": "\<base64-encoded zip\>"}, "version": N}
The zip contains a single JSON file with the full artifact structure.
"""

url = f'https://dp.aidevops.{region}.api.aws/asset/agent-space/{agent_space_id}/assets/{asset_id}/content'

session = boto3.session.Session()
credentials = session.get_credentials().get_frozen_credentials()
request = AWSRequest(method='GET', url=url)
SigV4Auth(credentials, 'aidevops', region).add_auth(request)

req = urllib.request.Request(url, headers=dict(request.headers), method='GET')
resp = urllib.request.urlopen(req, timeout=15)
body = json.loads(resp.read())

zip_bytes = base64.b64decode(body['content']['zipFile'])
z = zipfile.ZipFile(io.BytesIO(zip_bytes))
for name in z.namelist():
return json.loads(z.read(name).decode('utf-8'))
return None
> Finding the artifact Asset ID: Agents that produce artifacts update the same artifact on every run using update_artifact. The artifact's knowledge_item_id (e.g. ki-91c8d9f1-...) is stable and permanent. Find it once by scanning the journal tool call results for list_artifacts response content, then store it as a Lambda environment variable (ARTIFACT_ASSET_ID). All future runs will update the same artifact.
> When asset APIs land in boto3: Watch for list_assets and get_asset_content to appear in the devops-agent service model. When available, they will replace the direct HTTP approach.

Retrieving Output from Journal Records (fallback)

If your agent does not produce an artifact, or as a fallback, you can extract the agent's final text response from the journal:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import boto3, json

devops = boto3.client('devops-agent', region_name='us-east-1')

def get_summary_from_journal(agent_space_id, execution_id):
records = []
kwargs = {'agentSpaceId': agent_space_id, 'executionId': execution_id}
while True:
resp = devops.list_journal_records(**kwargs)
# Note: SDK returns key 'records', not 'journalRecords'
batch = resp.get('journalRecords') or resp.get('records') or []
records.extend(batch)
if not resp.get('nextToken'):
break
kwargs['nextToken'] = resp['nextToken']

# Records are type 'message' containing conversation JSON
# The final assistant text message is the agent's conclusion
texts = []
for r in sorted(records, key=lambda x: x.get('createdAt', '')):
if r.get('recordType') != 'message':
continue
try:
msg = json.loads(r.get('content', '{}'))
if msg.get('role') == 'assistant':
for item in msg.get('content', []):
if item.get('type') == 'text' and item.get('text', '').strip():
texts.append(item['text'].strip())
except Exception:
pass
return texts[-1] if texts else '(No output found)'

Lambda Integration: Complete Pattern

Lambda receives the EventBridge event, polls GetBacklogTask until completion, fetches the artifact, and takes action. This example triggers sns to notify.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
```python
import boto3, json, os, time, base64, zipfile, io, urllib.request
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

REGION = os.environ.get('AWS_REGION', 'us-east-1')
SNS_TOPIC_ARN = os.environ['SNS_TOPIC_ARN'] # e.g. arn:aws:sns:us-east-1:123456789012:my-topic
AGENT_NAME = os.environ.get('AGENT_NAME', 'my-custom-agent')
ARTIFACT_ID = os.environ.get('ARTIFACT_ASSET_ID', '') # stable ki-* ID, set once after first run

POLL_INTERVAL = 30 # seconds between status checks
MAX_POLLS = 25 # 25 × 30s = ~12.5 min maximum wait

devops = boto3.client('devops-agent', region_name=REGION)
sns = boto3.client('sns', region_name=REGION)

def lambda_handler(event, context):
# 1. Extract taskId from the CloudTrail event
task = event['detail']['responseElements']['task']
task_id = task['taskId']
agent_space_id = task['agentSpaceId']
created_at = task.get('createdAt', 'N/A')

# 2. Poll GetBacklogTask until the agent reaches a terminal state
terminal = {'COMPLETED', 'FAILED', 'TIMED_OUT', 'CANCELLED'}
detail = {}
for i in range(MAX_POLLS):
resp = devops.get_backlog_task(agentSpaceId=agent_space_id, taskId=task_id)
detail = resp.get('backlogTask') or resp.get('task') or {}
status = detail.get('status', 'UNKNOWN')
print(f"Poll {i+1}: {status}")
if status in terminal:
break
time.sleep(POLL_INTERVAL)

execution_id = detail.get('executionId')
updated_at = detail.get('updatedAt', 'N/A')

# 3. Fetch the artifact via Data Plane API (preferred)
report = '(No output)'
if status == 'COMPLETED' and ARTIFACT_ID:
try:
artifact = get_asset_content(agent_space_id, ARTIFACT_ID)
report = format_artifact(artifact)
except Exception as e:
print(f"Asset API failed: {e}, falling back to journal")
if execution_id:
report = get_summary_from_journal(agent_space_id, execution_id)
elif status == 'COMPLETED' and execution_id:
report = get_summary_from_journal(agent_space_id, execution_id)

# 4. Take action — email via SNS (swap for Jira, Slack, Step Functions, etc.)
sns.publish(
TopicArn=SNS_TOPIC_ARN,
Subject=f"[{AGENT_NAME}] {status}"[:100],
Message=(
f"Agent: {AGENT_NAME}\n"
f"Status: {status}\n"
f"Started: {created_at}\n"
f"Completed: {updated_at}\n\n"
f"{report}"
)
)
return {'statusCode': 200, 'body': 'OK'}

def get_asset_content(agent_space_id, asset_id):
"""Fetch artifact via DevOps Agent Data Plane API (dp. endpoint)."""
url = f'https://dp.aidevops.{REGION}.api.aws/asset/agent-space/{agent_space_id}/assets/{asset_id}/content'
credentials = boto3.session.Session().get_credentials().get_frozen_credentials()
request = AWSRequest(method='GET', url=url)
SigV4Auth(credentials, 'aidevops', REGION).add_auth(request)
resp = urllib.request.urlopen(urllib.request.Request(url, headers=dict(request.headers)), timeout=15)
body = json.loads(resp.read())
zip_bytes = base64.b64decode(body['content']['zipFile'])
z = zipfile.ZipFile(io.BytesIO(zip_bytes))
for name in z.namelist():
return json.loads(z.read(name).decode('utf-8'))
return None

def format_artifact(artifact):
"""Convert artifact JSON elements into readable text for email."""
lines = []
for el in artifact.get('elements', []):
if el.get('type') == 'text':
lines.append(el.get('content', ''))
elif el.get('type') in ('data_table', 'table'):
lines.append(f"\n### {el.get('title', 'Table')}")
cols = el.get('columns', [])
headers = [c.get('label', c.get('key', '')) for c in cols]
lines.append(' | '.join(headers))
lines.append(' | '.join(['---'] * len(headers)))
for row in el.get('data', [])[:20]:
lines.append(' | '.join(str(row.get(c.get('key', ''), '')) for c in cols))
elif el.get('type') == 'chart':
lines.append(f"\n### {el.get('title', 'Chart')}")
for d in el.get('data', []):
vals = list(d.values())
lines.append(f" {vals[0]}: {vals[1] if len(vals) > 1 else ''}")
lines.append('')
return '\n'.join(lines)

def get_summary_from_journal(agent_space_id, execution_id):
"""Fallback: extract final assistant text from journal records."""
records, kwargs = [], {'agentSpaceId': agent_space_id, 'executionId': execution_id}
while True:
r = devops.list_journal_records(**kwargs)
batch = r.get('journalRecords') or r.get('records') or []
records.extend(batch)
if not r.get('nextToken'):
break
kwargs['nextToken'] = r['nextToken']
texts = []
for r in sorted(records, key=lambda x: x.get('createdAt', '')):
if r.get('recordType') != 'message':
continue
try:
msg = json.loads(r.get('content', '{}'))
if msg.get('role') == 'assistant':
for item in msg.get('content', []):
if item.get('type') == 'text' and item.get('text', '').strip():
texts.append(item['text'].strip())
except Exception:
pass
return texts[-1] if texts else '(No output found)'
```

What Actions Can Lambda Take?

Once Lambda has the agent's output, it can drive any downstream workflow. Just examples.
Email the report via SNS — simplest option, suitable for any agent that produces a text or artifact summary.
Create Jira tickets per finding — parse the artifact table rows and call the Jira REST API to create one issue per finding, labelled and assigned automatically.
Trigger certificate renewal — if the agent found expiring ACM certificates, call acm.renew_certificate() for each one.
Start a drift remediation Step Functions workflow — pass the structured artifact data to a state machine that applies fixes, validates them, and records the result.
Post to Slack — send the executive summary or a condensed version to a team channel via webhook.

Handling the Lambda 15-Minute Limit

Lambda has a hard maximum execution timeout of 15 minutes. If your custom agent runs longer — common for agents scanning large environments across many accounts and regions — Lambda will time out before polling completes and the output will be lost.

Option 1 — Decouple Trigger from Polling (recommended for most cases)

Use two Lambdas with DynamoDB as a handoff. The first Lambda receives the EventBridge event, saves the taskId to DynamoDB, and exits immediately. A second Lambda runs on a schedule every 2 minutes, checks DynamoDB for pending tasks, calls GetBacklogTask for each, and when it sees COMPLETED it fetches the artifact and takes action. Neither Lambda ever waits — they just check and exit. No timeout risk regardless of how long the agent runs.

Option 2 — AWS Step Functions (for complex multi-step workflows)

Step Functions supports Wait states that pause execution without consuming compute or hitting time limits. EventBridge triggers a Lambda that starts a Step Functions execution. The workflow waits 60 seconds, checks the task status, loops back if still running, and when complete fans out in parallel — sending an email, creating Jira tickets, triggering a remediation workflow — all as separate branches. Step Functions has no overall execution timeout (up to 1 year), so agent run duration is never a concern.
When to use which: For a simple action (email, one API call), the two-Lambda + DynamoDB pattern is simpler to operate. For complex downstream workflows with multiple parallel actions or conditional branching, use Step Functions.
📢 Published by the ES-TAM-Builder community.
Follow our ES-TAM-Builder  Space for more tools, guides, and technical deep-dives from fellow TAMs

Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article