Building an Ambient Agent

End-to-end reference architecture for a webhook-triggered ambient agent with cross-system orchestration.

View as Markdown

This guide walks through the full architecture of a realistic ambient agent — from webhook listener to notification with action buttons. It’s a reference architecture showing how every piece fits together.

The example: an Incident Response Agent that listens for PagerDuty incidents, classifies severity with an LLM, creates a tracking issue in Jira, and notifies the on-call engineer with options to acknowledge, escalate, or add a note.

The YAML and configurations here are pseudocode — realistic enough to teach the patterns, but not copy-pasteable into Agent Studio. Adapt them to your actual system’s API.

In practice, most of these components are configured through the Agent Studio UI — not written as code. We’re showing code representations here because it’s the clearest way to illustrate the full configuration in a document. When you build, you’ll fill in these same fields through the visual editor.

In Agent Studio, you create an ambient agent by setting a plugin’s Trigger Type to System. See Ambient Agents for the conceptual overview.


Architecture Overview

Component count: 2 connectors, 3 HTTP actions, 1 compound action (with switch, try-catch, LLM classification, and notify), 1 webhook listener, 1 plugin with system trigger.


Step 1: Connectors

Two external systems, two connectors.

1# Connector: PagerDuty
2name: pagerduty
3base_url: https://api.pagerduty.com
4auth_type: api_key
5 # Header: Authorization
6 # Value: Token token=YOUR_API_KEY
7 # Additional header: Accept: application/vnd.pagerduty+json;version=2
8
9# Connector: Jira Cloud
10name: jira_cloud
11base_url: https://yourcompany.atlassian.net
12auth_type: basic_auth
13 # Username: your-email@company.com
14 # Password: your-api-token

Step 2: HTTP Actions

Three actions across the two systems.

Acknowledge PagerDuty Incident

1# HTTP Action: acknowledge_pagerduty_incident
2name: acknowledge_pagerduty_incident
3connector: pagerduty
4method: PUT
5path: /incidents/{{incident_id}}
6headers:
7 From: {{actor_email}}
8input_args:
9 - name: incident_id
10 type: string
11 required: true
12 - name: actor_email
13 type: string
14 required: true
15 - name: note
16 type: string
17 required: false
18body:
19 incident:
20 type: '''incident_reference'''
21 status: '''acknowledged'''

Add Note to PagerDuty Incident

1# HTTP Action: add_pagerduty_note
2name: add_pagerduty_note
3connector: pagerduty
4method: POST
5path: /incidents/{{incident_id}}/notes
6headers:
7 From: {{actor_email}}
8input_args:
9 - name: incident_id
10 type: string
11 required: true
12 - name: actor_email
13 type: string
14 required: true
15 - name: content
16 type: string
17 required: true
18body:
19 note:
20 content: content

Create Jira Issue

1# HTTP Action: create_jira_issue
2name: create_jira_issue
3connector: jira_cloud
4method: POST
5path: /rest/api/2/issue
6input_args:
7 - name: summary
8 type: string
9 required: true
10 - name: description
11 type: string
12 required: true
13 - name: priority
14 type: string
15 required: true
16 - name: project_key
17 type: string
18 required: true
19body:
20 fields:
21 project:
22 key: project_key
23 summary: summary
24 description: description
25 issuetype:
26 name: '''Bug'''
27 priority:
28 name: priority

Step 3: Compound Action

This is the core orchestration. It receives the webhook payload, classifies the incident, routes by severity, creates a Jira issue for high/critical incidents, and notifies the on-call engineer.

1# Compound Action: handle_incident
2name: handle_incident
3input_args:
4 - name: incident_id
5 type: string
6 - name: incident_title
7 type: string
8 - name: service_name
9 type: string
10 - name: urgency
11 type: string
12 - name: assignee_email
13 type: string
14 - name: incident_url
15 type: string
16
17steps:
18 # Step 1: Classify severity using LLM
19 - action:
20 action_name: mw.generate_structured_value_action
21 output_key: classification
22 input_args:
23 system_prompt: >-
24 '''You are an incident triage specialist. Based on the incident
25 title, service, and urgency, classify the severity and recommend
26 an initial response action.'''
27 payload:
28 RENDER():
29 template: |
30 Incident: {{title}}
31 Service: {{service}}
32 Urgency: {{urgency}}
33 args:
34 title: data.incident_title
35 service: data.service_name
36 urgency: data.urgency
37 output_schema: >-
38 {
39 "type": "object",
40 "properties": {
41 "severity": {
42 "type": "string",
43 "enum": ["low", "medium", "high", "critical"]
44 },
45 "reasoning": { "type": "string" },
46 "recommended_action": { "type": "string" }
47 },
48 "required": ["severity", "reasoning", "recommended_action"],
49 "additionalProperties": false
50 }
51
52 # Step 2: Route by severity
53 - switch:
54 cases:
55 # Low severity — auto-acknowledge and add a note
56 - condition: data.classification.generated_output.severity == '''low'''
57 steps:
58 - action:
59 action_name: add_pagerduty_note
60 output_key: note_result
61 input_args:
62 incident_id: data.incident_id
63 actor_email: data.assignee_email
64 content:
65 RENDER():
66 template: >-
67 Auto-triaged as low severity. Reasoning: {{reasoning}}.
68 Recommended action: {{action}}.
69 args:
70 reasoning: data.classification.generated_output.reasoning
71 action: data.classification.generated_output.recommended_action
72 - return:
73 output_mapper:
74 status: '''auto_triaged'''
75 severity: '''low'''
76
77 # High or critical — create Jira issue, acknowledge, notify
78 default:
79 steps:
80 # Create a Jira tracking issue
81 - try_catch:
82 try:
83 steps:
84 - action:
85 action_name: create_jira_issue
86 output_key: jira_result
87 input_args:
88 summary:
89 $CONCAT(["[PD-", data.incident_id, "] ", data.incident_title])
90 description:
91 RENDER():
92 template: |
93 PagerDuty incident auto-created by Moveworks.
94
95 Service: {{service}}
96 Urgency: {{urgency}}
97 Severity: {{severity}}
98 Reasoning: {{reasoning}}
99
100 PagerDuty link: {{url}}
101 args:
102 service: data.service_name
103 urgency: data.urgency
104 severity: data.classification.generated_output.severity
105 reasoning: data.classification.generated_output.reasoning
106 url: data.incident_url
107 priority:
108 LOOKUP():
109 key: data.classification.generated_output.severity
110 mapping:
111 '''critical''': '''Highest'''
112 '''high''': '''High'''
113 '''medium''': '''Medium'''
114 default: '''Low'''
115 project_key: '''INCIDENT'''
116 progress_updates:
117 on_pending: Creating Jira tracking issue...
118 on_complete: Jira issue created.
119 catch:
120 steps:
121 - action:
122 action_name: add_pagerduty_note
123 output_key: fallback_note
124 input_args:
125 incident_id: data.incident_id
126 actor_email: data.assignee_email
127 content: '''Failed to create Jira issue. Manual triage required.'''
128
129 # Acknowledge PagerDuty incident with Jira link
130 - action:
131 action_name: acknowledge_pagerduty_incident
132 output_key: ack_result
133 input_args:
134 incident_id: data.incident_id
135 actor_email: data.assignee_email
136
137 # Resolve the on-call engineer as a Moveworks user
138 - action:
139 action_name: mw.get_user_by_email
140 output_key: oncall_user
141 input_args:
142 user_email: data.assignee_email
143
144 # Notify with action buttons
145 - notify:
146 output_key: notification_result
147 recipient_id: data.oncall_user.user.record_id
148 message:
149 RENDER():
150 template: |
151 🚨 **{{severity}} incident** on {{service}}
152
153 **{{title}}**
154
155 {{reasoning}}
156
157 Jira: {{jira_key}}
158 PagerDuty: {{pd_link}}
159 args:
160 severity: data.classification.generated_output.severity
161 service: data.service_name
162 title: data.incident_title
163 reasoning: data.classification.generated_output.recommended_action
164 jira_key: data.jira_result.key
165 pd_link: data.incident_url
166 actions:
167 - key: escalate
168 label: '''Escalate'''
169 system_action:
170 action_name: acknowledge_pagerduty_incident
171 input_args:
172 incident_id: data.incident_id
173 actor_email: data.assignee_email
174 note: '''Escalated via Moveworks'''
175 - key: add_note
176 label: '''Add Note'''
177 conversation_action:
178 conversation_process_name: collect_incident_note
179 input_args:
180 incident_id: data.incident_id
181 - key: view_jira
182 label: '''View Jira Issue'''
183 content_action:
184 message:
185 RENDER():
186 template: "[Open in Jira](https://yourcompany.atlassian.net/browse/{{key}})"
187 args:
188 key: data.jira_result.key

What’s happening in this compound action

  1. LLM classificationgenerate_structured_value_action returns structured JSON with severity, reasoning, and recommended action. The schema guarantees the output shape.

  2. Switch routing — Low severity gets auto-triaged (note + early return). Everything else goes through the full flow.

  3. Jira issue creation — wrapped in try_catch so a Jira failure doesn’t break the whole flow. On failure, a PagerDuty note is added instead.

  4. LOOKUP for priority mapping — converts our severity labels to Jira priority names.

  5. User resolutionmw.get_user_by_email converts the assignee email to a Moveworks user object (required for notify).

  6. Notify with 3 action button types:

    • system_action — triggers another action (escalate)
    • conversation_action — starts a conversational process (collect a note from the user)
    • content_action — displays a clickable link (view Jira issue)

Step 4: Webhook Listener

The listener receives PagerDuty webhook events and routes them to the plugin.

1# Webhook Listener: pagerduty_incidents
2name: pagerduty_incidents
3security:
4 verification_type: signature
5 algorithm: hmac_sha256
6 header: X-PagerDuty-Signature
7 # Signing secret configured in the UI

PagerDuty sends the signature in the format v1=<hex_digest>. Configure the signing secret in the listener security settings to verify webhook authenticity.

For testing, you can use an unsecured listener (no signature verification). For production, always enable signature verification.


Step 5: Plugin

1# Plugin: Incident Response Agent
2name: incident_response_agent
3description: >
4 Monitors PagerDuty incidents, classifies severity with AI,
5 creates Jira tracking issues, and notifies on-call engineers
6 with action buttons to escalate, add notes, or view details.
7
8trigger_type: system
9system_trigger:
10 type: webhook
11 listener: pagerduty_incidents
12
13body_type: compound_action
14body: handle_incident
15
16# Input mapping: webhook payload → compound action args
17input_mapping:
18 incident_id: parsed_body.event.data.id
19 incident_title: parsed_body.event.data.title
20 service_name: parsed_body.event.data.service.summary
21 urgency: parsed_body.event.data.urgency
22 assignee_email: parsed_body.event.data.assignments[0].assignee.summary
23 incident_url: parsed_body.event.data.html_url

Webhook data is accessed via parsed_body in the plugin input mapping. Inside the compound action, these values are available as data.<input_arg_name>.


Step 6: Testing

  1. Test HTTP actions individually — use the Test button to verify each PagerDuty and Jira action works with sample input.

  2. Test the compound action — use the Test button with sample input args mimicking the webhook payload:

    1{
    2 "incident_id": "PABC123",
    3 "incident_title": "Database connection pool exhausted",
    4 "service_name": "Production API",
    5 "urgency": "high",
    6 "assignee_email": "oncall@yourcompany.com",
    7 "incident_url": "https://yourcompany.pagerduty.com/incidents/PABC123"
    8}
  3. Test the webhook end-to-end — send a test payload to your listener URL:

    $curl -X POST "https://api.moveworks.ai/webhooks/v1/listeners/<LISTENER_ID>/notify" \
    > -H "Content-Type: application/json" \
    > -d '{
    > "event": {
    > "event_type": "incident.triggered",
    > "data": {
    > "id": "PABC123",
    > "title": "Database connection pool exhausted",
    > "status": "triggered",
    > "urgency": "high",
    > "service": { "summary": "Production API" },
    > "assignments": [{ "assignee": { "summary": "oncall@yourcompany.com" } }],
    > "html_url": "https://yourcompany.pagerduty.com/incidents/PABC123"
    > }
    > }
    > }'

For detailed testing and error handling guidance, see Testing & Error Handling.


Key Takeaways

ConceptHow it’s used here
Two connectorsPagerDuty (API token) and Jira (Basic Auth) — one per system
HTTP ActionsOne per API operation: acknowledge, add note, create issue
LLM Classificationgenerate_structured_value_action with a schema guarantees typed output (severity enum)
Switch routingLow severity gets early return; high/critical go through full flow
Try-catchJira failure doesn’t break the flow — falls back to a PagerDuty note
LOOKUPMaps severity labels to Jira priority names
User resolutionmw.get_user_by_email converts email to user object for notify
Notify + action buttonsThree button types: system_action, conversation_action, content_action
Webhook input mappingparsed_body.event.data.* maps webhook fields to compound action args
String constants'''incident_reference''', '''acknowledged''' use triple-quote syntax
Early returnLow severity branch uses return inside switch to exit the entire compound action

Variations

Scheduled instead of webhook: Replace the webhook trigger with a scheduled trigger that polls PagerDuty for open incidents on an interval. Test the compound action directly using the Test button since you can’t manually fire a schedule.

Approval before escalation: Instead of a system_action button for escalation, use mw.create_generic_approval_request to require manager approval before escalating. See Built-in Actions.

Multiple notification targets: Use mw.batch_get_users_by_email to resolve multiple users, then use a for loop inside the compound action to send individual notifications. Results come back in input order.

Bi-directional sync: When the Jira issue is resolved, fire a second webhook-triggered plugin that resolves the PagerDuty incident. Two plugins, two listeners, shared connectors.