***

title: Building a Conversational Plugin
position: 1
excerpt: End-to-end reference architecture for a multi-step conversational plugin.
deprecated: false
hidden: false
---------------------

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

This guide walks through the full architecture of a realistic conversational plugin — from connector to testing. It's not a step-by-step tutorial; it's a **reference architecture** showing how every piece fits together.

The example: an **Expense Report Manager** that lets employees look up their pending expense reports, add a new expense line item to a report, and submit a report for approval — all through a single conversational plugin backed by SAP Concur.

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

***

# Architecture Overview

Here's what you're building and how the pieces connect:

```mermaid
flowchart TB
    subgraph Plugin["Plugin: Manage Expense Reports"]
        Triggers["Natural Language Triggers"]
        CP["Conversational Process"]
    end

    subgraph Process["Conversational Process"]
        Slot1["Slot: expense_report — (custom type, dynamic resolver)"]
        Slot2["Slot: action_choice — (string, static resolver)"]
        DP["Decision Policy — (route by action_choice)"]

        subgraph ViewBranch["View Branch"]
            A1["Activity: Show Report Details — (content activity)"]
        end

        subgraph AddBranch["Add Expense Branch"]
            Slot3["Slot: amount"]
            Slot4["Slot: vendor"]
            Slot5["Slot: category"]
            A2["Activity: Add Expense — (compound action, confirmation ON)"]
        end

        subgraph SubmitBranch["Submit Branch"]
            A3["Activity: Submit Report — (compound action, confirmation ON)"]
        end
    end

    subgraph Actions["Actions"]
        CA1["Compound Action: — add_expense_to_report"]
        CA2["Compound Action: — submit_expense_report"]
        HTTP1["HTTP Action: — fetch_expense_reports"]
        HTTP2["HTTP Action: — create_expense_entry"]
        HTTP3["HTTP Action: — submit_report"]
    end

    subgraph External["External System"]
        Connector["Connector: — SAP Concur (OAuth 2.0)"]
        API["Concur Expense API v4"]
    end

    Triggers --> CP
    CP --> Slot1 --> Slot2 --> DP
    DP -->|"View"| ViewBranch
    DP -->|"Add Expense"| AddBranch
    DP -->|"Submit"| SubmitBranch
    AddBranch --> A2 --> CA1
    SubmitBranch --> A3 --> CA2
    CA1 --> HTTP2
    CA2 --> HTTP3
    Slot1 -.->|"resolver calls"| HTTP1
    HTTP1 --> Connector --> API
    HTTP2 --> Connector
    HTTP3 --> Connector
```

**Component count:** 1 connector, 3 HTTP actions, 2 compound actions, 1 custom data type with dynamic resolver, 1 conversational process with 5 slots and 3 branches.

***

# Step 1: Connector

The connector is the authentication layer. Every HTTP action inherits from it.

```yaml
# Connector: SAP Concur
name: sap_concur
base_url: https://us.api.concursolutions.com
auth_type: oauth_2_client_credentials
  # Client ID and secret configured in the UI
  # Token endpoint: https://us.api.concursolutions.com/oauth2/v0/token
```

<Callout intent="info">
  **One connector per system.** If your Concur instance uses one set of credentials for all expense APIs, you need one connector. Only create a second if a different module requires different credentials.
</Callout>

***

# Step 2: HTTP Actions

Three actions, each doing one thing:

## Fetch Expense Reports

Used by the dynamic resolver to retrieve the user's reports.

```yaml
# HTTP Action: fetch_expense_reports
name: fetch_expense_reports
connector: sap_concur
method: GET
path: /expensereports/v4/users/{{user_id}}/context/TRAVELER/reports
query_params:
  approvalStatus: PENDING
input_args:
  - name: user_id
    type: string
    required: true
```

**Response shape** (what the API returns):

```json
{
  "reports": [
    {
      "reportId": "764428DD6A664AF0BFCB",
      "name": "March Client Travel",
      "reportTotal": { "value": 1250.00, "currencyCode": "USD" },
      "approvalStatus": "Not Submitted",
      "reportDate": "2026-03-25",
      "expenseCount": 4
    }
  ]
}
```

## Create Expense Entry

Adds a new line item to a report.

```yaml
# HTTP Action: create_expense_entry
name: create_expense_entry
connector: sap_concur
method: POST
path: /expensereports/v4/users/{{user_id}}/context/TRAVELER/reports/{{report_id}}/expenses
input_args:
  - name: user_id
    type: string
    required: true
  - name: report_id
    type: string
    required: true
  - name: amount
    type: number
    required: true
  - name: currency
    type: string
    required: true
  - name: vendor
    type: string
    required: true
  - name: category
    type: string
    required: true
  - name: date
    type: string
    required: true
body:   # uses data mapper
  transactionAmount:
    value: amount
    currencyCode: currency
  vendor: vendor
  expenseTypeId: category
  transactionDate: date
```

## Submit Report

Changes the report's workflow status to submitted.

```yaml
# HTTP Action: submit_expense_report
name: submit_expense_report
connector: sap_concur
method: PATCH
path: /expensereports/v4/users/{{user_id}}/context/TRAVELER/reports/{{report_id}}/submit
input_args:
  - name: user_id
    type: string
    required: true
  - name: report_id
    type: string
    required: true
```

***

# Step 3: Custom Data Type + Dynamic Resolver

This is what lets the user say "my March travel expenses" and have the system resolve it to a specific report object.

## Custom Data Type

```yaml
# Data Type: u_ExpenseReport
name: u_ExpenseReport
description: An SAP Concur expense report
schema:   # generated from a sample API response
  reportId: string
  name: string
  reportTotal:
    value: number
    currencyCode: string
  approvalStatus: string
  reportDate: string
  expenseCount: integer
```

## Dynamic Resolver (attached to the data type)

```yaml
# Resolver Strategy: find_expense_reports
name: find_expense_reports
method_type: dynamic
action: fetch_expense_reports     # the HTTP action from step 2

input_mapping:
  user_id: meta_info.user.record_id     # current user's ID

output_mapping: response.reports        # point to the array in the response

output_cardinality: list_of_candidates  # user picks from a list
```

When the user says "my March expenses," the resolver calls the HTTP action, gets back a list of reports, and presents them for the user to pick from. The selected report object (with all fields) is stored in the slot.

<Callout intent="info">
  A data type can have **multiple resolver strategies**. For example, you could add a "search by report ID" resolver for users who say "report 764428." The AI agent picks the best resolver based on context. See [Resolver Strategies](/agent-studio/conversation-process/resolver-strategies#where-to-configure-resolvers).
</Callout>

***

# Step 4: Compound Actions

Compound actions keep intermediate data out of the Reasoning Engine's context window. Only the `return` data goes back. This is why you use them instead of chaining action activities.

## Add Expense to Report

```yaml
# Compound Action: add_expense_to_report
input_args:
  - name: report_id
    type: string
  - name: user_id
    type: string
  - name: amount
    type: number
  - name: vendor
    type: string
  - name: category
    type: string

steps:
  - action:
      action_name: create_expense_entry
      output_key: expense_result
      input_args:
        user_id: data.user_id
        report_id: data.report_id
        amount: data.amount
        currency: '''USD'''                # string constant — see common patterns
        vendor: data.vendor
        category: data.category
        date: $TIME().$FORMAT_TIME("yyyy-MM-dd")
      progress_updates:
        on_pending: Adding expense to your report...
        on_complete: Expense added.

  - return:
      output_mapper:
        result:
          expense_id: data.expense_result.expenseId
          amount: data.amount
          vendor: data.vendor
          message: '''Expense added successfully.'''
```

## Submit Expense Report

```yaml
# Compound Action: submit_expense_report
input_args:
  - name: report_id
    type: string
  - name: user_id
    type: string
  - name: report_name
    type: string

steps:
  - try_catch:
      try:
        steps:
          - action:
              action_name: submit_expense_report
              output_key: submit_result
              input_args:
                user_id: data.user_id
                report_id: data.report_id
              progress_updates:
                on_pending: Submitting your report for approval...
                on_complete: Report submitted.
      catch:
        on_status_code: [400, 403]
        steps:
          - raise:
              output_key: submit_error
              message: >-
                Could not submit {{report_name}}. The report may be
                missing required fields or you may not have permission.

  - return:
      output_mapper:
        result:
          report_name: data.report_name
          status: '''Submitted'''
          message: '''Your report has been submitted for approval.'''
```

***

# Step 5: Conversational Process

This is where you define the user-facing flow — what slots to collect, what actions to run, and what decisions to make.

## Slots

```yaml
slots:
  # Slot 1: Which report?
  - name: expense_report
    type: u_ExpenseReport            # custom data type from step 3
    description: The expense report the user wants to manage
    inference_policy: infer_if_available
    resolver: find_expense_reports   # inherited from the data type

  # Slot 2: What do you want to do?
  - name: action_choice
    type: string
    description: What the user wants to do with the report
    inference_policy: infer_if_available
    resolver:
      type: static
      options:
        - display: View report details
          value: view
        - display: Add a new expense
          value: add
        - display: Submit for approval
          value: submit

  # Slot 3-5: Only needed for the "add expense" branch
  - name: amount
    type: number
    description: Dollar amount of the expense
    validation_policy: value > 0

  - name: vendor
    type: string
    description: Name of the merchant or vendor

  - name: category
    type: string
    description: Expense category
    resolver:
      type: static
      options:
        - display: Meals & Dining
          value: MEALS
        - display: Transportation
          value: TRANS
        - display: Lodging
          value: LODGE
        - display: Office Supplies
          value: OFFIC
```

## Decision Policy + Activities

After collecting `expense_report` and `action_choice`, a decision policy routes to the right branch:

```yaml
decision_policy:
  required_slots: [expense_report, action_choice]
  cases:
    - condition: data.action_choice.value == "view"
      # Content activity — show the report details
      activity:
        type: content
        text: |
          **{{expense_report.name}}**
          Status: {{expense_report.approvalStatus}}
          Total: ${{expense_report.reportTotal.value}} {{expense_report.reportTotal.currencyCode}}
          Expenses: {{expense_report.expenseCount}}
          Date: {{expense_report.reportDate}}

    - condition: data.action_choice.value == "add"
      # Action activity — collect remaining slots, then call compound action
      activity:
        type: action
        action: add_expense_to_report
        required_slots: [expense_report, amount, vendor, category]
        confirmation_policy: enabled    # user reviews before committing
        input_mapping:
          report_id: data.expense_report.reportId
          user_id: meta_info.user.record_id
          amount: data.amount
          vendor: data.vendor
          category: data.category.value

    - condition: data.action_choice.value == "submit"
      # Action activity — submit the report
      activity:
        type: action
        action: submit_expense_report
        required_slots: [expense_report]
        confirmation_policy: enabled
        input_mapping:
          report_id: data.expense_report.reportId
          user_id: meta_info.user.record_id
          report_name: data.expense_report.name
```

<Callout intent="warning">
  **Input and output mappers are configured on action activities** — not on the plugin or the HTTP action. This is where you map slot values and metadata to your action's input arguments. See [Activities](/agent-studio/conversation-process/activities).
</Callout>

***

# Step 6: Plugin

The plugin ties everything together with triggers and launch configuration.

```yaml
# Plugin: Manage Expense Reports
name: manage_expense_reports
description: >
  Look up your pending expense reports, add new expenses, or submit
  reports for approval. Connects to SAP Concur.

trigger_type: conversational
triggering_utterances:
  - Check my expense reports
  - Add an expense to my report
  - Submit my expense report
  - What's the status of my expenses
  - I need to file an expense

body: expense_report_process    # the conversational process from step 5

launch_configuration:
  access: all_users             # or restrict to specific groups
```

***

# Step 7: Testing

1. **Test your HTTP actions** individually using the Test button — verify they return the expected response shape.
2. **Test your compound actions** using the Test button — enter sample input args and check logs.
3. **Test the full plugin** through the AI assistant:
   * "Check my expense reports" → should show your pending reports
   * Pick a report → "Add an expense" → fill amount, vendor, category → confirm
   * "Submit my March report" → confirm → should submit

For detailed testing guidance, see [Testing & Error Handling](/agent-studio/guides/testing-and-error-handling).

***

# Key Takeaways

| Concept                    | How it's used here                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------------------- |
| **Connector**              | One connector for all Concur API calls (OAuth 2.0)                                                       |
| **HTTP Actions**           | One per API operation (fetch, create, submit) — each does one thing                                      |
| **Custom Data Type**       | `u_ExpenseReport` with a dynamic resolver so users can say "my March expenses"                           |
| **Compound Actions**       | Wrap write operations (add expense, submit report) to keep intermediate data out of the Reasoning Engine |
| **Conversational Process** | Slots collect input, decision policy routes by user intent, action activities call compound actions      |
| **Confirmation Policy**    | Enabled on write operations (add, submit) so the user reviews before committing                          |
| **Decision Policy**        | Routes to view/add/submit branches based on the `action_choice` slot                                     |
| **Input Mapping**          | On action activities — maps slot values to compound action input args                                    |
| **Error Handling**         | `try_catch` in the submit compound action handles 400/403 gracefully                                     |
| **String Constants**       | `'''USD'''` and `'''Submitted'''` use triple-quote syntax for literal values                             |

***

# Variations

**Multiple systems:** If you also need to sync expenses to an ERP, add a second connector and chain the ERP call inside the compound action after the Concur call.

**Approval flow:** To add manager approval before submission, use `mw.create_generic_approval_request` in the compound action. See [Built-in Actions](/agent-studio/actions/built-in-actions#create_generic_approval_request).

**SDA for analytics:** If users ask "how much did I spend this quarter?", the report list response may be large enough to trigger [Structured Data Analysis](/agent-studio/core-concepts/structured-data-analysis). Use friendly field names in your output mapper.