> This page is for Tools.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.moveworks.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.moveworks.com/_mcp/server.

# Quickstart #6: Expense Approval Ambient Agent

# What You'll Build

You'll create a **smart expense approval agent** that:

* **Auto-approves** expenses **under \$500**
* **Routes** expenses **\$500+** to the right manager with a **chat-based approval request**
* **Updates** your expense system (via Purple API) with **approved/denied**

The result is a streamlined process for everyone. For expenses over \$500, managers receive a simple, direct approval request right in their chat client, allowing them to take action in seconds.

![](https://files.readme.io/b376e7953688525d56f5e7d8417ef310d518b37f65cbc3e159e8a4764b1b05ef-CleanShot_2025-09-04_at_22.13.38.png)

While the user-facing interaction is intentionally simple, the real power is the automation happening behind the scenes. The plugin processes every request from your expense system, and for the majority of expenses (those under \$500), no manual action is needed at all — they are approved instantly.

# Learning Objectives

* Build a **Listener** to receive expense webhooks
* Create a **Compound Action** with **conditional logic** (`switch`)
* **Implement an automated approval branch** for expenses under a set threshold.
* Use **built-in actions**: `batch_get_users`, `generate_text`, `create_generic_approval_request`
* Wire everything in a **Plugin** with **input mapping**
* **Update the source system** to reflect the final "approved" or "rejected" status.

# Prerequisites

1. Access to the "Tool Studio" App
2. Completed [Quickstart #5: Notification Ambient Agent](/tool-studio/quickstart-guide/webhook-triggers-quickstart-guide)

# Architecture Overview

The plugin's logic is built around a central Compound Action that is triggered by a Listener. This Compound Action ingests data from the expense system, such as the approver, the requester, and the expense details, and then follows a conditional path based on the expense amount.

```mermaid
graph TD
    A[Webhook Trigger] --> B[Listener]
    B --> D

    subgraph compound [Handle Expense Compound Action]
        D{expense > $500}
        E[Action: mw.batch_get_users_by_email]
        F[Action: mw.generate_text_action]
        G[Action: mw.create_generic_approval_request]
        H{Approval}
        I[Action: update_approvalRequest +]
        J[Action: update_approvalRequest -]
        K[Action: update_approvalRequest +]

        D -->|Yes| E
        E --> F
        F --> G
        G --> H
        H -->|Approve| I
        H -->|Reject| J

        D -->|No| K
    end

    subgraph inputs [Inputs]
        L[approver]
        M[business_justification]
        N[requested_by]
        O[payload]
    end

    style A fill:#ff9999,stroke:#333
    style B fill:#99ccff,stroke:#333
    style compound fill:#ccffcc,stroke:#333
    style D fill:#ffebcc,stroke:#333
    style E fill:#99ccff,stroke:#333
    style F fill:#99ccff,stroke:#333
    style G fill:#99ccff,stroke:#333
    style H fill:#ffebcc,stroke:#333
    style I fill:#99ff99,stroke:#333
    style J fill:#ff9999,stroke:#333
    style K fill:#99ff99,stroke:#333
    style L fill:#ffff99,stroke:#333
    style M fill:#ffff99,stroke:#333
    style N fill:#ffff99,stroke:#333
    style O fill:#ffff99,stroke:#333

    classDef input fill:#ffff99,stroke:#333
    class L,M,N,O input
```

*Let's get started!*

# Phase 0: Set Up the Purple Suite

> **Already set up?**
>
> If you've already completed the [Purple Suite Setup](/tool-studio/quickstart-guide/purple-suite-setup) and have your connector configured, skip to Phase 1.

Follow the [Purple Suite Setup](/tool-studio/quickstart-guide/purple-suite-setup) guide to:

1. Create a Purple Suite instance at [marketplace.moveworks.com/purple-suite](https://marketplace.moveworks.com/purple-suite)
2. Get your **Instance ID** and **API Token (PAT)**
3. Set up a connector in Tool Studio with API Key auth

# Phase 1: Set up your "Update Approval Status" Action

Let's set up an Action to update the status of the approval that is being provided by the source system. This action will require two dynamic inputs: `approval_id` and `status`.

#### Create a new HTTP Action

Navigate to a new HTTP Action.

![](https://files.readme.io/f766e3b729ab041efe71fc7f811d7086cab20363a6e32e74738aa10c75965e6f-Screenshot_2025-04-11_at_22.27.04.png)

#### Set the title and description

Set the following title and description for your Action (be sure to replace "first" and "last" with your corresponding information).

| Field           | Value to enter (replace "first" & "last" with your info) |
| :-------------- | :------------------------------------------------------- |
| **Title**       | `quickstart_first_last_update_approval_request`          |
| **Description** | `Updates an approval request's status`                   |

#### Import the API details

Enter the details of your API:

1. Click on the "Import" icon to the right of the "TEST" button.

   ![](https://files.readme.io/211a66734c7e49958b56459da201f7c13d5db1969a9aa20538baef768b0bd03d-Screenshot_2025-04-11_at_15.39.40.png)
2. Paste the following cURL command:
   ```curl
   curl -X PATCH "https://marketplace.moveworks.com/api/purple-suite/erp/approvals/{{approval_id}}" \
     -H "Authorization: Bearer YOUR_PAT" \
     -H "X-Instance-ID: YOUR_INSTANCE_ID" \
     -H "Content-Type: application/json" \
     -d '{"status": "{{status}}"}'
   ```
3. Click "Import" (your Action should now be auto-populated with details).

#### Add your connector

Navigate to the "Connector" tab, and select your existing Moveworks Purple Connector (set up in Phase 0).

![](https://files.readme.io/577d1124c1b8d3917418a3802e6df0c71f271221567f603cd8ff4d36d9dd0bd2-CleanShot_2025-09-05_at_10.06.47.png)

#### Define input arguments

Define 2 formal input arguments to represent the "approval ID" and "status" inputs that this Action requires:

1. Click on the "Input Args" button near the top right corner.
2. Click "Create New" in the "Input Arguments" pop up.
3. Fill out the following details for your `approval_id` argument:

   | Field label       | Value to enter/select                          |
   | :---------------- | :--------------------------------------------- |
   | **Argument Name** | `approval_id`                                  |
   | **Data Type**     | *Select "string"*                              |
   | **Example Value** | `APV-965`                                      |
   | **Description**   | `The ID of the approval request being updated` |
   | **Required**      | *Check the box*                                |
4. Hit "Save".
5. Click "Create New" again in the "Input Arguments" pop up.
6. Fill out the following details for your `status` argument:

   | Field label       | Value to enter/select                                 |
   | :---------------- | :---------------------------------------------------- |
   | **Argument Name** | `status`                                              |
   | **Data Type**     | *Select "string"*                                     |
   | **Example Value** | `approved`                                            |
   | **Description**   | `The status the approval request is being updated to` |
   | **Required**      | *Check the box*                                       |
7. Hit "Save" and hit the "X" icon to close this "Input Arguments" pop up.

#### Publish

Hit "Publish".

> **Nice, your Action is all set up and ready to update approval requests!**
>
> Time to construct your Compound Action.

# Phase 2: Create Your Compound Action

You will now create a compound action to handle the incoming webhook routed by the listener.

In this phase you will build a compound action with 1) `approver`, `business_justification`, `requested_by`, `payload` as input arguments, 2) three actions to resolve the users by email, create a Moveworks approval request, and update the approval in the source system, and 3) switch expressions to conditionally route the expense approval requests. Here's a reminder of the bird's-eye view for our Compound Action:

```mermaid
graph TD

    subgraph compound [Handle Expense Compound Action]
        D{expense > $500}
        E[Action: mw.batch_get_users_by_email]
        F[Action: mw.generate_text_action]
        G[Action: mw.create_generic_approval_request]
        H{Approval}
        I[Action: update_approvalRequest +]
        J[Action: update_approvalRequest -]
        K[Action: update_approvalRequest +]

        D -->|Yes| E
        E --> F
        F --> G
        G --> H
        H -->|Approve| I
        H -->|Reject| J

        D -->|No| K
    end

    subgraph inputs [Inputs]
        L[approver]
        M[business_justification]
        N[requested_by]
        O[payload]
    end

    style compound fill:#ccffcc,stroke:#333
    style D fill:#ffebcc,stroke:#333
    style E fill:#99ccff,stroke:#333
    style F fill:#99ccff,stroke:#333
    style G fill:#99ccff,stroke:#333
    style H fill:#ffebcc,stroke:#333
    style I fill:#99ff99,stroke:#333
    style J fill:#ff9999,stroke:#333
    style K fill:#99ff99,stroke:#333
    style L fill:#ffff99,stroke:#333
    style M fill:#ffff99,stroke:#333
    style N fill:#ffff99,stroke:#333
    style O fill:#ffff99,stroke:#333

    classDef input fill:#ffff99,stroke:#333
    class L,M,N,O input
```

#### Create a new Compound Action

Navigate to a new "Compound Action" in the left nav and then click "Create".

![](https://files.readme.io/8bd159bb6868ab120b655909f5250a9d0063f70b4f2b5a958c70ca44ca58b574-CleanShot_2025-09-02_at_08.45.53.png)

#### Set the title and description

Set the following title and description for your Compound Action (be sure to replace "firstname" and "lastname" with your corresponding information).

| Field           | Value                                          |
| :-------------- | :--------------------------------------------- |
| **Title**       | `quickstart_first_last_expense_approval_ca`    |
| **Description** | `Handles an incoming expense approval request` |

#### Configure the Compound Action expression

Copy the following expression into the Compound Action Editor:

> **Warning**
>
> Make sure to replace the action name's with your action name!

```yaml
steps:
    - switch:
        cases:
            - steps: 
                - action:
                    output_key: early_approval_result
                    action_name: quickstart_first_last_update_approval_request # MAKE SURE TO CHANGE THIS
                    input_args:
                      approval_id: data.payload.approval_id
                      status: '''approved'''

                - return:
                    output_mapper: 
                        early_return_message: '''Approval request approved. Terminating process'''
              condition: data.payload.amount < 500
        default:
            steps: []
    - action:
        output_key: target_users
        action_name: mw.batch_get_users_by_email
        input_args:
          user_emails: [data.approver, data.requested_by]
    - action:
        output_key: llm_summary_result
        action_name: mw.generate_text_action
        input_args:
          system_prompt: '''Based on the provided business justification and payload from an approval expense request. Write a brief summary for the approver of the request with the important details that an approver must see. Do not format with *.'''
          user_input: 
            RENDER():
                template: "business justification: {{business_justification}}\n payload: {{webhook_data}}"
                args:
                    webhook_data: $STRINGIFY_JSON(data.payload)
                    business_justification: data.business_justification
    - action:
        output_key: approval_request_result
        action_name: mw.create_generic_approval_request
        input_args:
          approvers: 
            - data.target_users.user_records[0].user
          users_requested_for: 
            - data.target_users.user_records[-1].user
          approval_details: data.llm_summary_result.generated_output
    - switch:
        cases:
            - steps: 
                - action:
                    output_key: reject_approval_result
                    action_name: quickstart_first_last_update_approval_request # MAKE SURE TO CHANGE THIS
                    input_args:
                      approval_id: data.payload.approval_id
                      status: '''denied'''

              condition: data.approval_request_result.status == "DENIED"
        default:
            steps: 
                - action:
                    output_key: approve_approval_result
                    action_name: quickstart_first_last_update_approval_request # MAKE SURE TO CHANGE THIS
                    input_args:
                      approval_id: data.payload.approval_id
                      status: '''approved'''

```

> **Info**
>
> **Understanding the Compound Action (Guide #6)**
>
> This Compound Action routes an expense-approval flow. It can **early-approve** low-risk requests, then (for everything else) **fetches users**, **summarizes the request with an LLM**, **creates an approval**, and finally **updates the source system** based on the approver's decision.
>
> 1. `switch` (early decision) **[(Syntax)](/tool-studio/actions/compound-actions/compound-action-syntax-reference#switch-conditional-statements)**
>    1. **Purpose:** Short-circuit the workflow for small requests.
>    2. **Process:** If `data.payload.amount < 500`, call your custom action `quickstart_first_last_update_approval_request` to **approve** immediately, then `return` with an `early_return_message` and stop the flow.
>    3. Otherwise, fall through to the default branch (do nothing here) and continue.
> 2. mw\.batch_get_users_by_email\` **[(Built-in Action)](/tool-studio/actions/built-in-actions#batch_get_users_by_email)**
>    1. **Purpose:** Resolve both the approver and the requester to Moveworks user objects.
>    2. **Inputs:** `user_emails: [data.approver, data.requested_by]` (provided by your trigger mapping).
>    3. **Output:** `data.target_users.user_records[0].user` (approver) and `[1].user` (requester).
> 3. `mw.generate_text_action` **[(Built-in Action)](/tool-studio/actions/built-in-actions#generate_text_action)**
>    1. **Purpose:** Create a crisp summary for the approver.
>    2. **Process:**
>       1. `system_prompt` asks the LLM to produce an approver-ready summary.
>       2. user_input`uses`RENDER()`to pass both the **business justification** and the **raw webhook payload** (stringified via`\$STRINGIFY_JSON(data.payload)\`).
>    3. **Output:** `data.llm_summary_result.generated_output` (the formatted approval details).
> 4. `mw.create_generic_approval_request` **[(Built-in Action)](/tool-studio/actions/built-in-actions#create_generic_approval_request)**
>    1. **Purpose:** Open an approval with the resolved participants and LLM summary.
>    2. **Inputs:**
>       1. `approvers`: `[ data.target_users.user_records[0].user ]`
>       2. `users_requested_for`: `[ data.target_users.user_records[1].user ]`
>       3. `approval_details`: `data.llm_summary_result.generated_output`
>    3. **Output:** `data.approval_request_result.status` indicates if the approver **Approved** or **Denied**.
> 5. `switch` (post-decision update) **[(Syntax)](/tool-studio/actions/compound-actions/compound-action-syntax-reference#switch-conditional-statements)**
>    1. **Purpose:** Sync the final decision back to your source system.
>       1. **Process:**
>          1. If `data.approval_request_result.status == "DENIED"`, call your custom action `quickstart_first_last_update_approval_request` with `status: 'denied'`.
>       2. **Default:** Call the same custom action with `status: 'approved'`.
>       3. **Inputs to custom action:**
>          1. `approval_id: data.payload.approval_id` (from the incoming webhook),
>          2. `status: 'approved' | 'denied'`.

#### Define input arguments

Define 4 formal input arguments to represent the `approver`, `requested_by`, `payload`, and `business_justification` inputs that this Compound Action requires.

1. Click on the "Input Args" button near the top right corner.

   ![](https://files.readme.io/1e37264a377e14044304d7960ed5828968c7bd6476884383a9c04e485ca5c1f7-CleanShot_2025-09-02_at_08.56.34.png)
2. Click "Create New" in the "Input Arguments" pop up.
3. Fill out the following details for your `approver` argument:

   | Field label       | Value to enter/select                           |
   | :---------------- | :---------------------------------------------- |
   | **Argument Name** | `approver`                                      |
   | **Data Type**     | *Select "string"*                               |
   | **Example Value** | `test_email@example.com`                        |
   | **Description**   | `The expense approval request's approver email` |
   | **Required**      | *Check the box*                                 |
4. Hit "Save".
5. Click "Create New" again to the "Input Arguments" pop up.
6. Fill out the following details for your `requested_by` argument:

   | Field label       | Value to enter/select                         |
   | :---------------- | :-------------------------------------------- |
   | **Argument Name** | `requested_by`                                |
   | **Data Type**     | *Select "string"*                             |
   | **Example Value** | `john`                                        |
   | **Description**   | `The user who requested the expense approval` |
   | **Required**      | *Check the box*                               |
7. Click "Create New" again to the "Input Arguments" pop up.
8. Fill out the following details for your `payload` argument:

   | Field label       | Value to enter/select                      |
   | :---------------- | :----------------------------------------- |
   | **Argument Name** | `payload`                                  |
   | **Data Type**     | *Select "object"*                          |
   | **Example Value** | `{"approval_id":"appr_demo","amount":100}` |
   | **Description**   | `The payload data in the expense request`  |
   | **Required**      | *Check the box*                            |
9. Click "Create New" again to the "Input Arguments" pop up.
10. Fill out the following details for your `business_justification` argument:

    | Field label       | Value to enter/select                          |
    | :---------------- | :--------------------------------------------- |
    | **Argument Name** | `business_justification`                       |
    | **Data Type**     | *Select "string"*                              |
    | **Example Value** | `team outing`                                  |
    | **Description**   | `The expense request's business justification` |
    | **Required**      | *Check the box*                                |
11. Hit "Save" and hit the "X" icon to close this "Input Arguments" pop up.

#### Publish

Click "Publish".

> **All done with Actions!**
>
> You're ready to move on — time to create your webhook listener.

# Phase 3: Set Up Your Webhook Listener

#### Create a new Listener

Navigate to a new Listener and click "Create".

![](https://files.readme.io/f1eff0f6fc8a1f38bffdf50da8b303e581a930be2c7d3bbf9f61cb1149946292-CleanShot_2025-08-29_at_14.25.47.png)

#### Set the title and description

Set the following Title and Description for your listener (be sure to replace **firstname** and **lastname** with your corresponding information).

![](https://files.readme.io/5c216d03884b09e266e752cacb202843f83fed83cfed94edf73f988ba4865960-CleanShot_2025-08-29_at_14.41.47.png)

| Field           | Value to enter (replace "first" & "last" with your info) |
| :-------------- | :------------------------------------------------------- |
| **Title**       | `quickstart_first_last_expense_approval_listener`        |
| **Description** | `Listens for expense approvals`                          |

#### Copy the webhook URL

Click "Copy URL" and paste the webhook URL somewhere you can get back to it quick, we will need it in future steps.

#### Publish

1. Click "Publish" to save your listener.
2. When prompted with a confirmation message about security, click "Publish anyway" to proceed.

![](https://files.readme.io/3b599dcd469e3439bd346df2a28d0ea48cef481e82aba04d3ac79230f1cc0705-CleanShot_2025-10-05_at_22.07.56.png)

> **Info**
>
> **Note:** Unsecured listeners are intended for testing only. For production environments, always enable signature verification to secure your webhook listener.

# Phase 4: Configure and Launch Your Plugin

This section guides you through adding your Listener and Compound Action to a **Plugin**. A key step will be configuring the data flow, where you'll specify exactly which fields from the webhook you want to forward to your action.

#### Create a new Plugin

Navigate to a new Plugin (navigate to the library and click "Create").

![](https://files.readme.io/3dc80b34aa167ed521edb3b70f28538db7528fd1c94ac3b498aa1420d741cf27-image.png)

#### Set the title and description

Set the following title and description for your Plugin (be sure to replace "first" and "last" with your corresponding information).

| Field           | Value to enter                                  |
| :-------------- | :---------------------------------------------- |
| **Title**       | `quickstart_first_last_expense_approval`        |
| **Description** | `Handles an incoming expense approval request.` |

#### Configure the trigger and body

In the "Workflow Overview" section click "System" in the Trigger box.

![](https://files.readme.io/acda56803fb61470e49986e7c50a9ed4f2187cbccda21f9934ca90ed5c3d9028-CleanShot_2025-09-02_at_09.35.41.png)

1. Configure your system trigger to connect your Listener in the slide-out pop up with:

   | Field Label             | Value to enter/select                                              |
   | :---------------------- | :----------------------------------------------------------------- |
   | **Select Trigger Type** | `Webhook`                                                          |
   | **Select Listener**     | `quickstart_first_last_expense_approval_listener` *(Select yours)* |
   | **Event Filter**        | *Leave this field empty*                                           |
2. Click on the "Body" in the "Workflow Overview

   ![](https://files.readme.io/7a9616790d77d90664cd93489448cd09620d91b63e681a6936f9e34e126f7bca-CleanShot_2025-09-02_at_09.35.41.png)
3. Configure your system body to connect your compound action to the plugin

   * **Select Type** - System body
   * **Select an Action** - quickstart_first_last_expense_approval_ca *(Select your Action)*
   * **Input Mapping** - Copy the below values
     * **approver: parsed_body.approver**
     * **payload: parsed_body.data**
     * **requested_by: parsed_body.requested_by**
     * **business_justification: parsed_body.details**

   \


   > **Info**
   >
   > **Understanding the Input Mapping**
   >
   > When the listener receives a webhook, we expect a JSON payload structured like this:
   >
   > ```json
   > {
   >   "approver": "jdoe@example.com",
   >   "data": {
   >     "amount": 300,
   >     "approval_id": "APV-965",
   >     "category": "Meals & Entertainment",
   >     "currency": "USD",
   >     "description": "Lunch meeting with a client",
   >     "merchant": "The Corner Bistro",
   >     "payment_method": "Corporate Credit Card",
   >     "transaction_date": "2025-09-05T10:26:00Z"
   >   },
   >   "details": "Requirement to advance to the next sales pipeline with client",
   >   "requested_by": "kjoe@example.com"
   > }
   > ```
   >
   > The listener automatically parses this JSON payload into a `parsed_body` object. To access the data you need, you simply use dot notation to reference the keys:
   >
   > * `parsed_body.email`
   > * `parsed_body.data`
   >
   > This method gives you the flexibility to extract only the specific data your compound action requires, even if the webhook sends a much larger payload.
   >
   > ***Note**: While this mapping is currently a manual process, we are developing an "Auto Configure" feature to streamline this setup in the future.*

#### Publish your Plugin

Click "Publish" to launch your Plugin.

> **And just like that — you've finished building your Plugin, and now it's ready to use!**
>
> Congrats! You are now ready to test your plugin. In the next section, you will be provided with required tools to help you test it.

# Phase 5: Testing

This section guides you through the Moveworks Purple API tool to help you test your plugin by sending webhooks to your listener.

#### Open the Webhooks tab

1. Go back to the tab where your [Moveworks Purple API Tool](/tool-studio/quickstart-guide/purple-api-tool) is open.
2. Click on the **Webhooks** Tab.

![](https://files.readme.io/c97ce7e4a39d1f05c3cdd9029392751efe7ad1bcaa37abf1562ad9c9caa9457a-CleanShot_2025-09-02_at_10.30.09.png)

#### Configure webhook fields

Configure the field values with your listener information:

| Field           | Value to enter                                                                                                                                                   |
| :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Webhook URL** | *Enter the Webhook URL you created in [Phase 3](/tool-studio/quickstart-guide/quickstart-6-expense-approval-ambient-agent#phase-3-set-up-your-webhook-listener)* |
| **API Key**     | *Leave empty*                                                                                                                                                    |

#### Set up the Approval

1. Click on the **Approvals** Tab.
2. Fill out the Approval Form with your information (Make sure to edit the approver and requested by fields):

   | Field label                | Value to enter/select                                           |
   | :------------------------- | :-------------------------------------------------------------- |
   | **Business Justification** | `Requirement to advance to the next sales pipeline with client` |
   | **Approver Email**         | `approver@example.com` *(Type approver email)*                  |
   | **Requested By Email**     | `requestor@example.com` *(Type requested by email)*             |
   | **Data Template**          | `Expense Approval`                                              |
3. Click on **Generate Approval**. You will see an approval being generated in the system for your instance with the details you entered.

#### Generate and send the webhook

1. Click on **Generate Webhook Payload**. A payload will be generated for your upcoming webhook:

   ```json
   {
     "approver": "approver@example.com", // this will your approver make sure to edit
     "data": {
       "amount": 300,
       "approval_id": "APV-965",
       "category": "Meals & Entertainment",
       "currency": "USD",
       "description": "Lunch meeting with a client",
       "merchant": "The Corner Bistro",
       "payment_method": "Corporate Credit Card",
       "transaction_date": "2025-09-05T10:26:00Z"
     },
     "details": "Requirement to advance to the next sales pipeline with client",
     "requested_by": "requestor@example.com" // this will be your requested by user
   }
   ```
2. Make sure to tinker with the `amount` field to test different flows of your plugin.
3. Click on **Trigger Webhook**.

> **Success! Check the Approvals**
>
> Excellent work! You have successfully tested your plugin. Depending on the amount you set, your approval request may have gotten automatically approved or you may have gotten an approval request!
> You can click on the **Refresh Approvals** button in the Purple API Tool to see the status updates on the approvals!

> **Info**
>
> **Next step: Test your plugin.** See our [Testing & Error Handling guide](/tool-studio/guides/getting-started/testing-and-error-handling) for how to test, debug, and handle errors in production.