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.

1# Connector: SAP Concur
2name: sap_concur
3base_url: https://us.api.concursolutions.com
4auth_type: oauth_2_client_credentials
5 # Client ID and secret configured in the UI
6 # 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.

1# HTTP Action: fetch_expense_reports
2name: fetch_expense_reports
3connector: sap_concur
4method: GET
5path: /expensereports/v4/users/{{user_id}}/context/TRAVELER/reports
6query_params:
7 approvalStatus: PENDING
8input_args:
9 - name: user_id
10 type: string
11 required: true

Response shape (what the API returns):

1{
2 "reports": [
3 {
4 "reportId": "764428DD6A664AF0BFCB",
5 "name": "March Client Travel",
6 "reportTotal": { "value": 1250.00, "currencyCode": "USD" },
7 "approvalStatus": "Not Submitted",
8 "reportDate": "2026-03-25",
9 "expenseCount": 4
10 }
11 ]
12}

Create Expense Entry

Adds a new line item to a report.

1# HTTP Action: create_expense_entry
2name: create_expense_entry
3connector: sap_concur
4method: POST
5path: /expensereports/v4/users/{{user_id}}/context/TRAVELER/reports/{{report_id}}/expenses
6input_args:
7 - name: user_id
8 type: string
9 required: true
10 - name: report_id
11 type: string
12 required: true
13 - name: amount
14 type: number
15 required: true
16 - name: currency
17 type: string
18 required: true
19 - name: vendor
20 type: string
21 required: true
22 - name: category
23 type: string
24 required: true
25 - name: date
26 type: string
27 required: true
28body: # uses data mapper
29 transactionAmount:
30 value: amount
31 currencyCode: currency
32 vendor: vendor
33 expenseTypeId: category
34 transactionDate: date

Submit Report

Changes the report’s workflow status to submitted.

1# HTTP Action: submit_expense_report
2name: submit_expense_report
3connector: sap_concur
4method: PATCH
5path: /expensereports/v4/users/{{user_id}}/context/TRAVELER/reports/{{report_id}}/submit
6input_args:
7 - name: user_id
8 type: string
9 required: true
10 - name: report_id
11 type: string
12 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

1# Data Type: u_ExpenseReport
2name: u_ExpenseReport
3description: An SAP Concur expense report
4schema: # generated from a sample API response
5 reportId: string
6 name: string
7 reportTotal:
8 value: number
9 currencyCode: string
10 approvalStatus: string
11 reportDate: string
12 expenseCount: integer

Dynamic Resolver (attached to the data type)

1# Resolver Strategy: find_expense_reports
2name: find_expense_reports
3method_type: dynamic
4action: fetch_expense_reports # the HTTP action from step 2
5
6input_mapping:
7 user_id: meta_info.user.record_id # current user's ID
8
9output_mapping: response.reports # point to the array in the response
10
11output_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

1# Compound Action: add_expense_to_report
2input_args:
3 - name: report_id
4 type: string
5 - name: user_id
6 type: string
7 - name: amount
8 type: number
9 - name: vendor
10 type: string
11 - name: category
12 type: string
13
14steps:
15 - action:
16 action_name: create_expense_entry
17 output_key: expense_result
18 input_args:
19 user_id: data.user_id
20 report_id: data.report_id
21 amount: data.amount
22 currency: '''USD''' # string constant — see common patterns
23 vendor: data.vendor
24 category: data.category
25 date: $TIME().$FORMAT_TIME("yyyy-MM-dd")
26 progress_updates:
27 on_pending: Adding expense to your report...
28 on_complete: Expense added.
29
30 - return:
31 output_mapper:
32 result:
33 expense_id: data.expense_result.expenseId
34 amount: data.amount
35 vendor: data.vendor
36 message: '''Expense added successfully.'''

Submit Expense Report

1# Compound Action: submit_expense_report
2input_args:
3 - name: report_id
4 type: string
5 - name: user_id
6 type: string
7 - name: report_name
8 type: string
9
10steps:
11 - try_catch:
12 try:
13 steps:
14 - action:
15 action_name: submit_expense_report
16 output_key: submit_result
17 input_args:
18 user_id: data.user_id
19 report_id: data.report_id
20 progress_updates:
21 on_pending: Submitting your report for approval...
22 on_complete: Report submitted.
23 catch:
24 on_status_code: [400, 403]
25 steps:
26 - raise:
27 output_key: submit_error
28 message: >-
29 Could not submit {{report_name}}. The report may be
30 missing required fields or you may not have permission.
31
32 - return:
33 output_mapper:
34 result:
35 report_name: data.report_name
36 status: '''Submitted'''
37 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

1slots:
2 # Slot 1: Which report?
3 - name: expense_report
4 type: u_ExpenseReport # custom data type from step 3
5 description: The expense report the user wants to manage
6 inference_policy: infer_if_available
7 resolver: find_expense_reports # inherited from the data type
8
9 # Slot 2: What do you want to do?
10 - name: action_choice
11 type: string
12 description: What the user wants to do with the report
13 inference_policy: infer_if_available
14 resolver:
15 type: static
16 options:
17 - display: View report details
18 value: view
19 - display: Add a new expense
20 value: add
21 - display: Submit for approval
22 value: submit
23
24 # Slot 3-5: Only needed for the "add expense" branch
25 - name: amount
26 type: number
27 description: Dollar amount of the expense
28 validation_policy: value > 0
29
30 - name: vendor
31 type: string
32 description: Name of the merchant or vendor
33
34 - name: category
35 type: string
36 description: Expense category
37 resolver:
38 type: static
39 options:
40 - display: Meals & Dining
41 value: MEALS
42 - display: Transportation
43 value: TRANS
44 - display: Lodging
45 value: LODGE
46 - display: Office Supplies
47 value: OFFIC

Decision Policy + Activities

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

1decision_policy:
2 required_slots: [expense_report, action_choice]
3 cases:
4 - condition: data.action_choice.value == "view"
5 # Content activity — show the report details
6 activity:
7 type: content
8 text: |
9 **{{expense_report.name}}**
10 Status: {{expense_report.approvalStatus}}
11 Total: ${{expense_report.reportTotal.value}} {{expense_report.reportTotal.currencyCode}}
12 Expenses: {{expense_report.expenseCount}}
13 Date: {{expense_report.reportDate}}
14
15 - condition: data.action_choice.value == "add"
16 # Action activity — collect remaining slots, then call compound action
17 activity:
18 type: action
19 action: add_expense_to_report
20 required_slots: [expense_report, amount, vendor, category]
21 confirmation_policy: enabled # user reviews before committing
22 input_mapping:
23 report_id: data.expense_report.reportId
24 user_id: meta_info.user.record_id
25 amount: data.amount
26 vendor: data.vendor
27 category: data.category.value
28
29 - condition: data.action_choice.value == "submit"
30 # Action activity — submit the report
31 activity:
32 type: action
33 action: submit_expense_report
34 required_slots: [expense_report]
35 confirmation_policy: enabled
36 input_mapping:
37 report_id: data.expense_report.reportId
38 user_id: meta_info.user.record_id
39 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.

1# Plugin: Manage Expense Reports
2name: manage_expense_reports
3description: >
4 Look up your pending expense reports, add new expenses, or submit
5 reports for approval. Connects to SAP Concur.
6
7trigger_type: conversational
8triggering_utterances:
9 - Check my expense reports
10 - Add an expense to my report
11 - Submit my expense report
12 - What's the status of my expenses
13 - I need to file an expense
14
15body: expense_report_process # the conversational process from step 5
16
17launch_configuration:
18 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.