***

title: Building an Ambient Agent
position: 2
excerpt: End-to-end reference architecture for a webhook-triggered ambient agent with cross-system orchestration.
deprecated: false
hidden: false
---------------------

For clean Markdown of any page, append .md to the page URL. For a complete documentation index, see https://docs.moveworks.com/agent-studio/guides/architecture/llms.txt. For full documentation content, see https://docs.moveworks.com/agent-studio/guides/architecture/llms-full.txt.

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.

<Callout intent="info">
  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.
</Callout>

<Callout intent="note">
  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.
</Callout>

<Callout intent="info">
  In Agent Studio, you create an ambient agent by setting a plugin's **Trigger Type** to **System**. See [Ambient Agents](/agent-studio/core-concepts/ambient-agents) for the conceptual overview.
</Callout>

***

# Architecture Overview

```mermaid
flowchart TB
    subgraph Trigger["Webhook Trigger"]
        PD["PagerDuty — incident.triggered"]
        Listener["Webhook Listener — (HMAC-SHA256 verification)"]
    end

    subgraph Plugin["Plugin: Incident Response Agent"]
        InputMapping["Input Mapping — (parsed_body → compound action args)"]
        CA["Compound Action: — handle_incident"]
    end

    subgraph CompoundAction["Compound Action Steps"]
        S1["Step 1: Classify severity — (mw.generate_structured_value_action)"]
        S2["Step 2: Switch on severity"]

        subgraph LowPath["Low Severity"]
            S3a["Add note to PagerDuty — (auto-acknowledged)"]
        end

        subgraph HighPath["High / Critical Severity"]
            S3b["Create Jira issue"]
            S4b["Acknowledge PagerDuty incident — with Jira link"]
            S5b["Resolve on-call engineer"]
            S6b["Notify with action buttons"]
        end
    end

    subgraph External["External Systems"]
        ConnPD["Connector: PagerDuty — (API Token)"]
        ConnJira["Connector: Jira — (Basic Auth)"]
        PDAPI["PagerDuty API"]
        JiraAPI["Jira REST API"]
    end

    PD -->|"POST webhook"| Listener
    Listener --> InputMapping --> CA
    CA --> S1 --> S2
    S2 -->|"low"| LowPath
    S2 -->|"high / critical"| HighPath
    S3a --> ConnPD --> PDAPI
    S3b --> ConnJira --> JiraAPI
    S4b --> ConnPD
    S5b -->|"mw.get_user_by_email"| S6b
```

**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.

```yaml
# 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

```yaml
# 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

```yaml
# 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

```yaml
# 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.

```yaml
# 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 classification** — `generate_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 resolution** — `mw.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.

```yaml
# 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.

<Callout intent="warning">
  For testing, you can use an unsecured listener (no signature verification). For production, always enable signature verification.
</Callout>

***

# Step 5: Plugin

```yaml
# 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
```

<Callout intent="info">
  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>`.
</Callout>

***

# 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:
   ```json
   {
     "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:
   ```bash
   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](/agent-studio/guides/testing-and-error-handling).

***

# Key Takeaways

| Concept                     | How it's used here                                                                       |
| --------------------------- | ---------------------------------------------------------------------------------------- |
| **Two connectors**          | PagerDuty (API token) and Jira (Basic Auth) — one per system                             |
| **HTTP Actions**            | One per API operation: acknowledge, add note, create issue                               |
| **LLM Classification**      | `generate_structured_value_action` with a schema guarantees typed output (severity enum) |
| **Switch routing**          | Low severity gets early return; high/critical go through full flow                       |
| **Try-catch**               | Jira failure doesn't break the flow — falls back to a PagerDuty note                     |
| **LOOKUP**                  | Maps severity labels to Jira priority names                                              |
| **User resolution**         | `mw.get_user_by_email` converts email to user object for `notify`                        |
| **Notify + action buttons** | Three button types: `system_action`, `conversation_action`, `content_action`             |
| **Webhook input mapping**   | `parsed_body.event.data.*` maps webhook fields to compound action args                   |
| **String constants**        | `'''incident_reference'''`, `'''acknowledged'''` use triple-quote syntax                 |
| **Early return**            | Low 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](/agent-studio/actions/built-in-actions#create_generic_approval_request).

**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.