Building a Conversational Plugin

End-to-end reference architecture for a multi-step conversational plugin.

View as Markdown

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.

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.


Architecture Overview

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

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.

# 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

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.


Step 2: HTTP Actions

Three actions, each doing one thing:

Fetch Expense Reports

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

# 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):

{
"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.

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

# 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

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

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

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.


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

# 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

# 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

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:

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

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.


Step 6: Plugin

The plugin ties everything together with triggers and launch configuration.

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


Key Takeaways

ConceptHow it’s used here
ConnectorOne connector for all Concur API calls (OAuth 2.0)
HTTP ActionsOne per API operation (fetch, create, submit) — each does one thing
Custom Data Typeu_ExpenseReport with a dynamic resolver so users can say “my March expenses”
Compound ActionsWrap write operations (add expense, submit report) to keep intermediate data out of the Reasoning Engine
Conversational ProcessSlots collect input, decision policy routes by user intent, action activities call compound actions
Confirmation PolicyEnabled on write operations (add, submit) so the user reviews before committing
Decision PolicyRoutes to view/add/submit branches based on the action_choice slot
Input MappingOn action activities — maps slot values to compound action input args
Error Handlingtry_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.

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. Use friendly field names in your output mapper.