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

Webhook Trigger Plugin: Incident Response Agent Compound Action Steps Low Severity High / Critical Severity External Systems low high / critical POST webhook mw.get_user_by_email PagerDuty — incident.triggered Webhook Listener — (HMAC-SHA256verification) Input Mapping — (parsed_body →compound action args) Compound Action: — handle_incident Step 1: Classify severity —(mw.generate_structured_value_action) Step 2: Switch on severity Add note to PagerDuty —(auto-acknowledged) Create Jira issue Acknowledge PagerDuty incident —with Jira link Resolve on-call engineer Notify with action buttons Connector: PagerDuty — (API Token) Connector: Jira — (Basic Auth) PagerDuty API Jira REST API

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.

# Connector: PagerDuty
name: pagerduty
base_url: https://api.pagerduty.com
auth_type: api_key
# Header: Authorization
# Value: Token token=YOUR_API_KEY
# Additional header: Accept: application/vnd.pagerduty+json;version=2
# Connector: Jira Cloud
name: jira_cloud
base_url: https://yourcompany.atlassian.net
auth_type: basic_auth
# Username: your-email@company.com
# Password: your-api-token

Step 2: HTTP Actions

Three actions across the two systems.

Acknowledge PagerDuty Incident

# HTTP Action: acknowledge_pagerduty_incident
name: acknowledge_pagerduty_incident
connector: pagerduty
method: PUT
path: /incidents/{{incident_id}}
headers:
From: {{actor_email}}
input_args:
- name: incident_id
type: string
required: true
- name: actor_email
type: string
required: true
- name: note
type: string
required: false
body:
incident:
type: '''incident_reference'''
status: '''acknowledged'''

Add Note to PagerDuty Incident

# HTTP Action: add_pagerduty_note
name: add_pagerduty_note
connector: pagerduty
method: POST
path: /incidents/{{incident_id}}/notes
headers:
From: {{actor_email}}
input_args:
- name: incident_id
type: string
required: true
- name: actor_email
type: string
required: true
- name: content
type: string
required: true
body:
note:
content: content

Create Jira Issue

# HTTP Action: create_jira_issue
name: create_jira_issue
connector: jira_cloud
method: POST
path: /rest/api/2/issue
input_args:
- name: summary
type: string
required: true
- name: description
type: string
required: true
- name: priority
type: string
required: true
- name: project_key
type: string
required: true
body:
fields:
project:
key: project_key
summary: summary
description: description
issuetype:
name: '''Bug'''
priority:
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.

# Compound Action: handle_incident
name: handle_incident
input_args:
- name: incident_id
type: string
- name: incident_title
type: string
- name: service_name
type: string
- name: urgency
type: string
- name: assignee_email
type: string
- name: incident_url
type: string
steps:
# Step 1: Classify severity using LLM
- action:
action_name: mw.generate_structured_value_action
output_key: classification
input_args:
system_prompt: >-
'''You are an incident triage specialist. Based on the incident
title, service, and urgency, classify the severity and recommend
an initial response action.'''
payload:
RENDER():
template: |
Incident: {{title}}
Service: {{service}}
Urgency: {{urgency}}
args:
title: data.incident_title
service: data.service_name
urgency: data.urgency
output_schema: >-
{
"type": "object",
"properties": {
"severity": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
},
"reasoning": { "type": "string" },
"recommended_action": { "type": "string" }
},
"required": ["severity", "reasoning", "recommended_action"],
"additionalProperties": false
}
# Step 2: Route by severity
- switch:
cases:
# Low severity — auto-acknowledge and add a note
- condition: data.classification.generated_output.severity == '''low'''
steps:
- action:
action_name: add_pagerduty_note
output_key: note_result
input_args:
incident_id: data.incident_id
actor_email: data.assignee_email
content:
RENDER():
template: >-
Auto-triaged as low severity. Reasoning: {{reasoning}}.
Recommended action: {{action}}.
args:
reasoning: data.classification.generated_output.reasoning
action: data.classification.generated_output.recommended_action
- return:
output_mapper:
status: '''auto_triaged'''
severity: '''low'''
# High or critical — create Jira issue, acknowledge, notify
default:
steps:
# Create a Jira tracking issue
- try_catch:
try:
steps:
- action:
action_name: create_jira_issue
output_key: jira_result
input_args:
summary:
$CONCAT(["[PD-", data.incident_id, "] ", data.incident_title])
description:
RENDER():
template: |
PagerDuty incident auto-created by Moveworks.
Service: {{service}}
Urgency: {{urgency}}
Severity: {{severity}}
Reasoning: {{reasoning}}
PagerDuty link: {{url}}
args:
service: data.service_name
urgency: data.urgency
severity: data.classification.generated_output.severity
reasoning: data.classification.generated_output.reasoning
url: data.incident_url
priority:
LOOKUP():
key: data.classification.generated_output.severity
mapping:
'''critical''': '''Highest'''
'''high''': '''High'''
'''medium''': '''Medium'''
default: '''Low'''
project_key: '''INCIDENT'''
progress_updates:
on_pending: Creating Jira tracking issue...
on_complete: Jira issue created.
catch:
steps:
- action:
action_name: add_pagerduty_note
output_key: fallback_note
input_args:
incident_id: data.incident_id
actor_email: data.assignee_email
content: '''Failed to create Jira issue. Manual triage required.'''
# Acknowledge PagerDuty incident with Jira link
- action:
action_name: acknowledge_pagerduty_incident
output_key: ack_result
input_args:
incident_id: data.incident_id
actor_email: data.assignee_email
# Resolve the on-call engineer as a Moveworks user
- action:
action_name: mw.get_user_by_email
output_key: oncall_user
input_args:
user_email: data.assignee_email
# Notify with action buttons
- notify:
output_key: notification_result
recipient_id: data.oncall_user.user.record_id
message:
RENDER():
template: |
🚨 **{{severity}} incident** on {{service}}
**{{title}}**
{{reasoning}}
Jira: {{jira_key}}
PagerDuty: {{pd_link}}
args:
severity: data.classification.generated_output.severity
service: data.service_name
title: data.incident_title
reasoning: data.classification.generated_output.recommended_action
jira_key: data.jira_result.key
pd_link: data.incident_url
actions:
- key: escalate
label: '''Escalate'''
system_action:
action_name: acknowledge_pagerduty_incident
input_args:
incident_id: data.incident_id
actor_email: data.assignee_email
note: '''Escalated via Moveworks'''
- key: add_note
label: '''Add Note'''
conversation_action:
conversation_process_name: collect_incident_note
input_args:
incident_id: data.incident_id
- key: view_jira
label: '''View Jira Issue'''
content_action:
message:
RENDER():
template: "[Open in Jira](https://yourcompany.atlassian.net/browse/{{key}})"
args:
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.

# Webhook Listener: pagerduty_incidents
name: pagerduty_incidents
security:
verification_type: signature
algorithm: hmac_sha256
header: X-PagerDuty-Signature
# 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

# Plugin: Incident Response Agent
name: incident_response_agent
description: >
Monitors PagerDuty incidents, classifies severity with AI,
creates Jira tracking issues, and notifies on-call engineers
with action buttons to escalate, add notes, or view details.
trigger_type: system
system_trigger:
type: webhook
listener: pagerduty_incidents
body_type: compound_action
body: handle_incident
# Input mapping: webhook payload → compound action args
input_mapping:
incident_id: parsed_body.event.data.id
incident_title: parsed_body.event.data.title
service_name: parsed_body.event.data.service.summary
urgency: parsed_body.event.data.urgency
assignee_email: parsed_body.event.data.assignments[0].assignee.summary
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:

    {
    "incident_id": "PABC123",
    "incident_title": "Database connection pool exhausted",
    "service_name": "Production API",
    "urgency": "high",
    "assignee_email": "oncall@yourcompany.com",
    "incident_url": "https://yourcompany.pagerduty.com/incidents/PABC123"
    }
  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.