# Common Patterns

This guide provides implementation recipes for common nativeMsg experience patterns. Each recipe follows the format: **Problem → Normative approach → Complete JSON example → Notes**.

***

## 1. Greeting Workflow with Quick Replies

**Problem:** Display a welcome message with tappable reply options when a user initiates conversation.

**Normative approach:** Define a workflow with the `greeting` intent and a `send` action containing a `message` with a `quickReplies` array. The `welcomeMessageExecute` property at the root SHOULD reference this workflow so it also fires on channel invitation.

```json
{
  "name": "Greeting Pattern",
  "welcomeMessageExecute": "greeting",
  "workflows": [
    {
      "name": "greeting",
      "intents": ["greeting"],
      "expressions": ["hello", "hi", "hey", "start", "help", "menu"],
      "actions": [
        {
          "name": "send-greeting",
          "send": {
            "message": {
              "text": "Hi! I am the Acme assistant. What would you like to do?",
              "quickReplies": [
                {
                  "type": "text",
                  "title": "Track an order",
                  "payload": "track_order",
                  "execute": "order-tracking"
                },
                {
                  "type": "text",
                  "title": "Start a return",
                  "payload": "start_return",
                  "execute": "returns"
                },
                {
                  "type": "text",
                  "title": "Store locations",
                  "payload": "store_locations",
                  "execute": "find-stores"
                }
              ]
            }
          }
        }
      ]
    }
  ]
}
```

**Notes:** Quick reply chips disappear after the user selects one. Limit titles to 25 characters or fewer. Up to 11 quick replies are allowed per message.

***

## 2. Collecting User Input with `waitFor` and Attribute Storage

**Problem:** Prompt the user for a specific piece of information and store it for use in subsequent steps.

**Normative approach:** Use a `send` action to prompt, then a `waitFor` action to pause execution until the user responds. Set `content` to the attribute name. Add a `validation` action after capture to verify the format.

```json
{
  "name": "Input Capture Pattern",
  "workflows": [
    {
      "name": "collect-email",
      "intents": ["collect_email"],
      "actions": [
        {
          "name": "prompt-email",
          "send": {
            "message": {
              "text": "Please enter your email address so we can send you a confirmation."
            }
          }
        },
        {
          "name": "capture-email",
          "waitFor": {
            "data": "text",
            "content": "userEmail",
            "timeout": "5m",
            "executeOnTimeout": "email-timeout",
            "executeOnError": "email-error"
          }
        },
        {
          "name": "validate-email-format",
          "validation": {
            "content": "userEmail",
            "pattern": "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$",
            "executeOnError": "invalid-email"
          }
        },
        {
          "name": "confirm-email-captured",
          "send": {
            "message": {
              "text": "Got it! We will send your confirmation to {userEmail}.",
              "quickReplies": [
                {
                  "type": "text",
                  "title": "That is correct",
                  "payload": "email_confirmed",
                  "execute": "finalize-registration"
                },
                {
                  "type": "text",
                  "title": "Change email",
                  "payload": "change_email",
                  "execute": "collect-email"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "invalid-email",
      "intents": ["invalid_email_error"],
      "actions": [
        {
          "name": "send-invalid-email-message",
          "send": {
            "message": {
              "text": "That does not look like a valid email address. Please try again (e.g. name@example.com)."
            }
          }
        },
        {
          "name": "re-capture-email",
          "waitFor": {
            "data": "text",
            "content": "userEmail",
            "timeout": "5m",
            "executeOnTimeout": "email-timeout"
          }
        }
      ]
    }
  ]
}
```

**Notes:** The `content` attribute name MUST match `^[a-zA-Z][a-zA-Z0-9_]*$`. Values captured by `waitFor` are immediately available as `{attributeName}` placeholders in subsequent actions.

***

## 3. Conditional Branching with `conditions` and `goto`

**Problem:** Execute different actions depending on the value of an attribute or the channel type.

**Normative approach:** Add `conditions` to each guarded action. Use `goto` to transfer control to a named action or workflow when a condition is met. Actions without a matching condition are skipped; execution continues with the next action.

```json
{
  "name": "Conditional Branching Pattern",
  "workflows": [
    {
      "name": "tier-offer",
      "intents": ["view_offer"],
      "actions": [
        {
          "name": "fetch-account-tier",
          "send": {
            "request": {
              "url": "https://api.acme.com/v1/accounts/{contactPhone}/tier",
              "method": "GET",
              "headers": {
                "Authorization": "Bearer {apiToken}"
              },
              "response": "accountTier",
              "retries": 1,
              "fallback": "tier-fetch-error"
            }
          }
        },
        {
          "name": "route-premium",
          "conditions": [
            { "comparisons": [["accountTier", "==", "premium"]] }
          ],
          "goto": "premium-offer"
        },
        {
          "name": "route-standard",
          "conditions": [
            { "comparisons": [["accountTier", "==", "standard"]] }
          ],
          "goto": "standard-offer"
        },
        {
          "name": "send-default-offer",
          "send": {
            "message": {
              "text": "Check out our current promotions at acme.com/offers."
            }
          }
        }
      ]
    },
    {
      "name": "premium-offer",
      "intents": ["premium_offer_internal"],
      "actions": [
        {
          "name": "send-premium-offer",
          "send": {
            "message": {
              "text": "As a Premium member, you have exclusive access to our VIP sale — 30% off everything this weekend!",
              "buttons": [
                {
                  "type": "weburl",
                  "title": "Shop VIP sale",
                  "payload": "https://shop.acme.com/vip-sale"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "standard-offer",
      "intents": ["standard_offer_internal"],
      "actions": [
        {
          "name": "send-standard-offer",
          "send": {
            "message": {
              "text": "Here is a 15% discount code just for you: ACME15. Valid through Sunday.",
              "buttons": [
                {
                  "type": "weburl",
                  "title": "Shop now",
                  "payload": "https://shop.acme.com/offers"
                }
              ]
            }
          }
        }
      ]
    }
  ]
}
```

**Notes:** Once a `goto` fires, remaining actions in the current workflow are not evaluated. Use `execute` when you need to invoke a sub-workflow and then continue in the caller.

***

## 4. Calling an External API with `send.request` and Mapping the Response

**Problem:** Retrieve data from an external service and surface the response values in subsequent messages.

**Normative approach:** Use a `send.request` action with a `response` mapping object. Keys in the mapping are JSON response field names; values are the attribute names in which to store them. Specify a `fallback` workflow for error handling.

```json
{
  "name": "API Integration Pattern",
  "workflows": [
    {
      "name": "product-lookup",
      "intents": ["lookup_product"],
      "actions": [
        {
          "name": "prompt-product-id",
          "send": {
            "message": {
              "text": "Enter a product ID to look up (e.g. PRD-001)."
            }
          }
        },
        {
          "name": "capture-product-id",
          "waitFor": {
            "data": "text",
            "content": "productId",
            "timeout": "3m",
            "executeOnTimeout": "lookup-timeout"
          }
        },
        {
          "name": "fetch-product",
          "send": {
            "request": {
              "url": "https://api.acme.com/v1/products/{productId}",
              "method": "GET",
              "headers": {
                "Authorization": "Bearer {apiToken}",
                "Accept": "application/json"
              },
              "dataFormat": "json",
              "response": {
                "name": "productName",
                "price": "productPrice",
                "inStock": "productInStock",
                "imageUrl": "productImageUrl",
                "description": "productDescription"
              },
              "retries": 2,
              "fallback": "product-not-found"
            }
          }
        },
        {
          "name": "send-product-details",
          "send": {
            "message": {
              "title": "{productName}",
              "text": "{productDescription}\n\nPrice: ${productPrice}",
              "mediaType": "image",
              "media": "{productImageUrl}",
              "buttons": [
                {
                  "type": "postback",
                  "title": "Add to cart",
                  "payload": "cart_add_{productId}",
                  "execute": "add-to-cart"
                },
                {
                  "type": "weburl",
                  "title": "View full details",
                  "payload": "https://shop.acme.com/products/{productId}"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "product-not-found",
      "intents": ["product_not_found_error"],
      "actions": [
        {
          "name": "send-not-found-message",
          "send": {
            "message": {
              "text": "I could not find a product with ID {productId}. Please check the ID and try again.",
              "quickReplies": [
                {
                  "type": "text",
                  "title": "Try again",
                  "payload": "retry_lookup",
                  "execute": "product-lookup"
                }
              ]
            }
          }
        }
      ]
    }
  ]
}
```

**Notes:** The `response` mapping object maps top-level JSON response keys to attribute names. For nested structures, use `send.populate` after storing the raw response to an attribute.

***

## 5. Multi-Step Form Using Sequential `waitFor` Actions

**Problem:** Collect multiple pieces of information from the user across several turns.

**Normative approach:** Chain `send` + `waitFor` pairs sequentially within one workflow. Each `waitFor` stores its result in a uniquely named attribute. After all captures, submit or confirm.

```json
{
  "name": "Multi-Step Form Pattern",
  "workflows": [
    {
      "name": "return-request",
      "intents": ["start_return", "return_item"],
      "expressions": [
        "I want to return something",
        "start a return",
        "return an item",
        "get a refund"
      ],
      "actions": [
        {
          "name": "intro-return",
          "send": {
            "message": {
              "text": "I can help you start a return. I need a few details — it will only take a moment."
            }
          }
        },
        {
          "name": "ask-order-number",
          "send": {
            "message": {
              "text": "Step 1 of 3: What is your order number? (e.g. ACM-12345)"
            }
          }
        },
        {
          "name": "capture-order-number",
          "waitFor": {
            "data": "text",
            "content": "returnOrderNumber",
            "timeout": "5m",
            "executeOnTimeout": "form-timeout"
          }
        },
        {
          "name": "ask-return-reason",
          "send": {
            "message": {
              "text": "Step 2 of 3: What is the reason for your return?",
              "quickReplies": [
                { "type": "text", "title": "Wrong size", "payload": "wrong_size" },
                { "type": "text", "title": "Defective item", "payload": "defective" },
                { "type": "text", "title": "Changed my mind", "payload": "changed_mind" },
                { "type": "text", "title": "Wrong item received", "payload": "wrong_item" }
              ]
            }
          }
        },
        {
          "name": "capture-return-reason",
          "waitFor": {
            "data": ["text", "quick reply"],
            "content": "returnReason",
            "timeout": "5m",
            "executeOnTimeout": "form-timeout"
          }
        },
        {
          "name": "ask-preferred-resolution",
          "send": {
            "message": {
              "text": "Step 3 of 3: How would you like to resolve this?",
              "quickReplies": [
                { "type": "text", "title": "Full refund", "payload": "refund" },
                { "type": "text", "title": "Exchange", "payload": "exchange" },
                { "type": "text", "title": "Store credit", "payload": "store_credit" }
              ]
            }
          }
        },
        {
          "name": "capture-resolution",
          "waitFor": {
            "data": ["text", "quick reply"],
            "content": "returnResolution",
            "timeout": "5m",
            "executeOnTimeout": "form-timeout"
          }
        },
        {
          "name": "submit-return-request",
          "send": {
            "request": {
              "url": "https://api.acme.com/v1/returns",
              "method": "POST",
              "dataFormat": "json",
              "headers": {
                "Authorization": "Bearer {apiToken}",
                "Content-Type": "application/json"
              },
              "content": {
                "orderNumber": "{returnOrderNumber}",
                "reason": "{returnReason}",
                "resolution": "{returnResolution}",
                "contactPhone": "{contactPhone}"
              },
              "response": "returnCaseId",
              "retries": 2,
              "fallback": "return-submission-failed"
            }
          }
        },
        {
          "name": "confirm-return",
          "send": {
            "message": {
              "text": "Your return request has been submitted!\n\nCase ID: {returnCaseId}\nOrder: {returnOrderNumber}\nReason: {returnReason}\nResolution: {returnResolution}\n\nYou will receive an email confirmation shortly."
            }
          }
        }
      ]
    }
  ]
}
```

**Notes:** Accepting both `"text"` and `"quick reply"` in the `data` array allows users to either tap a chip or type a free-text response. Each `waitFor` blocks execution until the user responds or the timeout fires.

***

## 6. Transferring to a Human Agent Using `updateSettings`

**Problem:** Escalate a conversation to a live agent when the user requests it or when automated handling fails.

**Normative approach:** Set conversation priority to `"high"`, assign routing tags, send an internal note for context, and confirm to the user. Disabling the experience via `updateSettings.rcsExperience.enabled: false` stops further automated responses.

```json
{
  "name": "Human Escalation Pattern",
  "workflows": [
    {
      "name": "escalate-to-human",
      "intents": ["human_agent", "speak_to_agent", "escalate"],
      "expressions": [
        "I want to speak to a person",
        "connect me to an agent",
        "human please",
        "let me talk to someone",
        "real person"
      ],
      "actions": [
        {
          "name": "acknowledge-escalation",
          "send": {
            "message": {
              "text": "I am connecting you with a support agent now. Average wait time is under 2 minutes."
            }
          }
        },
        {
          "name": "set-high-priority",
          "updateSettings": {
            "conversation": {
              "priority": "high"
            }
          }
        },
        {
          "name": "tag-for-routing",
          "assignTags": ["escalated", "needs-agent", "human-requested"]
        },
        {
          "name": "store-escalation-context",
          "assignAttributes": {
            "attributes": [
              {
                "attributePath": "escalationTimestamp",
                "value": "{currentTimestamp}",
                "process": true
              },
              {
                "attributePath": "escalationTrigger",
                "value": "user_requested"
              }
            ]
          }
        },
        {
          "name": "notify-agent-queue",
          "send": {
            "note": {
              "text": "Escalation requested by {contactPhone} at {escalationTimestamp}. Reason: {escalationTrigger}.",
              "channelId": 15
            }
          }
        },
        {
          "name": "disable-bot",
          "updateSettings": {
            "rcsExperience": {
              "enabled": false
            }
          }
        }
      ]
    }
  ]
}
```

**Notes:** Setting `rcsExperience.enabled` to `false` prevents further automated responses, leaving the conversation open for a human agent. Re-enable it programmatically via the nativeMsg API when the agent session ends.

***

## 7. Carousel Product Listing

**Problem:** Display a horizontally scrollable set of products, each with an image, description, and action buttons.

**Normative approach:** Use a `send.message` with a `carousel` array. Each entry is a [RichCard](https://playbook.nativemsg.com/rcs-experience-schema/reference/messages) with `title`, `description`, `media`, and up to 4 `buttons`. A carousel MUST contain 2–10 cards.

```json
{
  "name": "Carousel Pattern",
  "workflows": [
    {
      "name": "show-featured-products",
      "intents": ["browse_products", "featured_items"],
      "expressions": [
        "show me products",
        "what do you have",
        "browse catalogue",
        "featured items"
      ],
      "actions": [
        {
          "name": "send-product-carousel",
          "send": {
            "message": {
              "text": "Here are our featured products this week:",
              "carousel": [
                {
                  "title": "Acme Pro Runner",
                  "description": "Lightweight carbon-fiber sole. Responsive cushioning. Available in 8 colors.",
                  "media": "https://cdn.acme.com/products/pro-runner-hero.jpg",
                  "mediaType": "image",
                  "mediaHeight": "MEDIUM",
                  "buttons": [
                    {
                      "type": "weburl",
                      "title": "View details",
                      "payload": "https://shop.acme.com/products/pro-runner"
                    },
                    {
                      "type": "postback",
                      "title": "Add to cart",
                      "payload": "cart_add_pro-runner",
                      "execute": "add-to-cart"
                    }
                  ]
                },
                {
                  "title": "Acme Trail Blazer",
                  "description": "All-terrain grip with waterproof membrane. 6 mm lug depth. Sizes 6–15.",
                  "media": "https://cdn.acme.com/products/trail-blazer-hero.jpg",
                  "mediaType": "image",
                  "mediaHeight": "MEDIUM",
                  "buttons": [
                    {
                      "type": "weburl",
                      "title": "View details",
                      "payload": "https://shop.acme.com/products/trail-blazer"
                    },
                    {
                      "type": "postback",
                      "title": "Add to cart",
                      "payload": "cart_add_trail-blazer",
                      "execute": "add-to-cart"
                    }
                  ]
                },
                {
                  "title": "Acme Urban Walker",
                  "description": "Memory foam insole. Slip-resistant outsole. Designed for all-day wear.",
                  "media": "https://cdn.acme.com/products/urban-walker-hero.jpg",
                  "mediaType": "image",
                  "mediaHeight": "MEDIUM",
                  "buttons": [
                    {
                      "type": "weburl",
                      "title": "View details",
                      "payload": "https://shop.acme.com/products/urban-walker"
                    },
                    {
                      "type": "postback",
                      "title": "Add to cart",
                      "payload": "cart_add_urban-walker",
                      "execute": "add-to-cart"
                    }
                  ]
                }
              ]
            }
          }
        }
      ]
    }
  ]
}
```

**Notes:** Each card supports up to 4 buttons. `mediaHeight` controls the image area: `SHORT` (\~112 dp), `MEDIUM` (\~168 dp), `TALL` (\~264 dp). Carousels are RCS-specific; use a list or sequential text messages as fallback on non-RCS channels.

***

## 8. RCS Rich Card with Channel Fallback

**Problem:** Send a visually rich message on RCS channels while delivering a plain-text fallback on channels that do not support rich cards.

**Normative approach:** Use the action `channel` property to restrict rich-content actions to RCS, and define parallel actions restricted to other channels for graceful degradation.

```json
{
  "name": "Channel Fallback Pattern",
  "workflows": [
    {
      "name": "send-promo-announcement",
      "intents": ["promo_announcement"],
      "actions": [
        {
          "name": "rich-card-rcs",
          "channel": "rcs",
          "send": {
            "message": {
              "title": "Spring Sale — Up to 40% Off",
              "text": "Shop our biggest sale of the year. This weekend only.",
              "mediaType": "image",
              "media": "https://cdn.acme.com/banners/spring-sale-2025.jpg",
              "richCard": {
                "cardOrientation": "VERTICAL",
                "mediaHeight": "TALL"
              },
              "buttons": [
                {
                  "type": "weburl",
                  "title": "Shop now",
                  "payload": "https://shop.acme.com/spring-sale"
                },
                {
                  "type": "postback",
                  "title": "Save for later",
                  "payload": "save_promo_spring2025",
                  "execute": "save-promo"
                }
              ]
            }
          }
        },
        {
          "name": "text-fallback-dsc",
          "channel": "dsc",
          "send": {
            "message": {
              "text": "Spring Sale — Up to 40% Off! Shop our biggest sale of the year this weekend only: https://shop.acme.com/spring-sale"
            }
          }
        },
        {
          "name": "text-fallback-dlc",
          "channel": "dlc",
          "send": {
            "message": {
              "text": "Acme Spring Sale: Up to 40% off this weekend. Shop at acme.com/spring-sale. Reply STOP to opt out."
            }
          }
        }
      ]
    }
  ]
}
```

**Notes:** The `channel` property on an action restricts execution to that channel type string exactly. Only one of the three actions above will fire per conversation turn, depending on the active channel. This pattern avoids duplicating entire workflows for channel-specific content.
