---
title: "'Ask Leo to..' Buttons (prefill Leo's chat from your app)"
slug: ask-leo-to-buttons
category: Integrations
summary: Buttons on your app's pages that drop a pre-written command straight into Leo's chat input via postMessage — one click instead of the user typing the request out.
tags: [postmessage, iframe, leo-chat, launchpad, javascript, stimulus, ux-default]
status: stable
visibility: public
source_project: mbc-preceptors.leo.llamapress.ai sign-in page
layers: [view, stimulus_js]
related:
  - title: "'Ask Leo to..' Buttons (customer guide)"
    url: /wiki/ask-leo-to-buttons
    summary: The customer-facing explanation of this pattern on the wiki — what the buttons do and how to ask Leo for one.
  - title: Leo Tools & Features
    url: /wiki/leo-tools-and-features
    summary: A tour of the chat interface these buttons talk to.
---

# 'Ask Leo to..' Buttons (prefill Leo's chat from your app)

> ⚠️ **Cookbook example — not live code.** (KEEP THIS CALLOUT.) Every code block below
> is an **example snippet**, **not part of the llamapress.ai codebase**, and **not
> running on this server**. This is a reference recipe for a **Leo instance (an AI coding
> agent) to implement in its own app** — read it to understand the pattern, then recreate
> it there.

Your app's pages render inside an iframe in the Leo workspace, with Leo's chat sitting
in the parent window. That parent listens for a specific `postMessage` shape — send it
and **Leo's chat input fills with your text, focused and ready to send**. An "Ask Leo
to.." button packages a whole request ("reset this password", "export this report",
"fix this page") into one click: the user reviews the pre-filled command, edits any
placeholder, and hits send.

The proven origin: a sign-in page with an **"Ask Leo to Reset"** button next to each
registered email. Clicking it pre-fills Leo's chat with
`Ask Leo to reset the password for jane@example.com to [new_password] so I can sign
into the application.` — the user swaps in a password and sends. No admin panel, no
console, and Leo (not hand-built form code) performs the action.

> **When to use:** actions that are really *requests for the AI agent* — resets, fixes,
> data changes, "do X for me" — where writing the prompt is the user's hurdle. The user
> stays in the loop: they see the command before it runs.
> **When not to:** actions your app should just *do* (a normal form/controller is
> faster and doesn't burn an agent turn), or anything on a page where visitors aren't
> Leo users — outside the workspace the buttons are silent no-ops.

<!-- ▼ LIVE DEMO block — renders interactively on the llamapress.ai/cookbook page
     (raw HTML passes through the markdown renderer). Agents: IGNORE this block;
     recreate the pattern from the code layers below. The real pattern posts to
     window.parent (the Leo workspace); this demo simulates the chat input inline
     because this page has no parent frame. Inline handlers only — an
     innerHTML-injected <script> never executes. -->
<div class="not-prose my-6 rounded-2xl border border-gray-200 overflow-hidden shadow-sm">
  <div class="bg-gray-50 border-b border-gray-200 px-4 py-2 flex items-center gap-2">
    <span class="w-2 h-2 rounded-full bg-emerald-400"></span>
    <span class="text-[10px] font-bold uppercase tracking-widest text-gray-500">Live demo — simulated (the real one fills Leo's actual chat)</span>
  </div>
  <div class="p-5 bg-white space-y-4">
    <div class="flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2">
      <span class="text-sm text-gray-700 truncate">jane@example.com</span>
      <button type="button" class="shrink-0 ml-2 text-xs bg-indigo-600 text-white px-2.5 py-1 rounded font-medium hover:bg-indigo-700 transition-colors" onclick="var d=document.getElementById('demo-leo-chat-input');d.value=this.dataset.command;d.focus();" data-command="Ask Leo to reset the password for jane@example.com to [new_password] so I can sign into the application.">Ask Leo to Reset</button>
    </div>
    <div>
      <p class="text-xs font-semibold text-gray-500 mb-1">Leo's chat input (simulated)</p>
      <textarea id="demo-leo-chat-input" rows="2" placeholder="Click the button above…" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-indigo-500"></textarea>
    </div>
  </div>
</div>

---

## The 80/20 in one breath

1. Put the full command text in a `data-command` attribute on a button with a shared
   class (e.g. `.ask-leo-btn`). Use `[bracketed_placeholders]` for anything the user
   must fill in.
2. On click, post `{ source: 'launchpad', type: 'prefill-chat', command }` to
   `window.parent` — that exact `source`/`type` pair is what Leo's chat listens for.
3. Guard with `window.parent !== window` so the button no-ops on the standalone site.
4. Done — the receiving side ships with every Leo workspace. It fills the chat input,
   focuses it, and dispatches an `input` event so the send button activates.
5. Optional: add `auto_send: true` to send immediately instead of pre-filling.

---

## Layer 1 — The View (button + inline script, the proven minimal version)

```erb
<%# app/views/devise/sessions/new.html.erb (or any page in your app) %>
<div class="flex items-center justify-between py-1.5 px-3 rounded hover:bg-slate-50 group">
  <span class="text-sm text-slate-700 truncate min-w-0">jane@example.com</span>
  <button class="ask-leo-btn shrink-0 ml-2 text-xs bg-indigo-600 text-white px-2.5 py-1 rounded font-medium hover:bg-indigo-700 transition-colors"
          data-command="Ask Leo to reset the password for jane@example.com to [new_password] so I can sign into the application.">
    Ask Leo to Reset
  </button>
</div>

<script>
  (function() {
    'use strict';
    document.querySelectorAll('.ask-leo-btn').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var command = this.dataset.command;
        if (window.parent !== window) {
          window.parent.postMessage({
            source: 'launchpad',      // exact value required
            type: 'prefill-chat',     // exact value required
            command: command          // lands in Leo's chat input verbatim
          }, '*');
        }
      });
    });
  })();
</script>
```

The message contract, in full:

| Field | Value | Notes |
|---|---|---|
| `source` | `'launchpad'` | Required, exact. Anything else is ignored by the listener. |
| `type` | `'prefill-chat'` | Required, exact. |
| `command` | your text | Placed into the chat input verbatim. |
| `auto_send` | `true` (optional) | Sends immediately instead of waiting for the user. Use sparingly. |

Target origin is `'*'` because Leo's chat interface runs on a different origin than
your app's pages. That's acceptable here — the message only pre-fills text and carries
nothing sensitive (see Gotchas).

## Layer 2 — Stimulus variant (house style, plays nicely with Turbo)

The inline script above re-binds fine on full page loads, but a Stimulus controller is
the idiomatic Rails/Hotwire shape and survives Turbo navigation without a re-run:

```javascript
// app/javascript/controllers/ask_leo_controller.js
import { Controller } from "@hotwired/stimulus"

// Sends a pre-written command into Leo's chat input (the parent workspace frame).
// Usage: <button data-controller="ask-leo"
//                data-action="ask-leo#send"
//                data-ask-leo-command-value="Ask Leo to ...">Ask Leo</button>
export default class extends Controller {
  static values = { command: String, autoSend: { type: Boolean, default: false } }

  send(event) {
    event.stopPropagation()
    if (window.parent === window) return   // standalone site: no-op
    window.parent.postMessage({
      source: "launchpad",
      type: "prefill-chat",
      command: this.commandValue,
      auto_send: this.autoSendValue
    }, "*")
  }
}
```

```erb
<%# anywhere in a view %>
<button data-controller="ask-leo"
        data-action="ask-leo#send"
        data-ask-leo-command-value="Ask Leo to export this month's report as a CSV.">
  Ask Leo to Export
</button>
```

## Layer 3 — What the receiving side does (context only — do NOT build this)

Every Leo workspace already ships the listener in its chat frontend. For understanding
(so you can debug, not so you can implement):

```javascript
// (ships with the Leo chat interface — reference only)
window.addEventListener('message', (event) => {
  if (event.data && event.data.source === 'launchpad' && event.data.type === 'prefill-chat') {
    const command = event.data.command;
    if (command && this.elements.messageInput) {
      this.elements.messageInput.value = command;
      this.elements.messageInput.focus();
      this.elements.messageInput.dispatchEvent(new Event('input', { bubbles: true }));
      if (event.data.auto_send) {
        this.sendMessageWithDebugInfo();
      }
    }
  }
});
```

---

## Gotchas (the hard-won stuff)

- **The `source`/`type` strings are an exact contract.** `'launchpad'` +
  `'prefill-chat'` — a typo (or `prefill_chat`) fails silently. There is no error
  channel; the message just doesn't match and nothing happens.
- **Silent no-op outside the workspace is a feature AND a trap.** The
  `window.parent !== window` guard means the button does nothing on the app's public
  URL. Keep the guard (never remove it), but if the page is public-facing, consider
  hiding the buttons or showing a hint when the page isn't embedded — otherwise real
  visitors click a dead button.
- **Never put secrets in `data-command`.** The attribute is plain page source, visible
  to anyone who can load the page. That's exactly why the password-reset command uses a
  `[new_password]` placeholder the user types into the chat, instead of generating a
  password into the button.
- **Prefer pre-fill over `auto_send`.** The pattern's strength is the human review step
  — the user sees and can edit the command before it runs. Reserve `auto_send: true`
  for commands with no placeholders and no surprises; a command containing
  `[brackets]` must never auto-send.
- **Write commands self-contained.** Leo reads the command with no other context from
  the button — bake the specifics (email, record, page) into the text and state the
  goal ("…so I can sign into the application"). Vague commands get vague agent turns.
- **`e.stopPropagation()` matters** when the button sits inside a clickable row/card —
  without it the row's own click handler (navigation, selection) fires too.
- **Cross-origin is normal here.** Your page and the chat interface are on different
  origins, so you cannot reach the chat DOM directly — `postMessage` with `'*'` is the
  supported channel. Don't try to tighten the target origin to your own host; the
  parent is *not* on your host.

---

## Files this pattern touches

```
app/views/<any_page>.html.erb                      # button(s) + inline script (Layer 1)
app/javascript/controllers/ask_leo_controller.js   # optional Stimulus variant (Layer 2)
```

## How to adapt to your schema

1. Decide the action and write the command sentence first — complete, specific,
   self-contained, with `[placeholders]` for user-supplied values.
2. Drop the button where the action lives (a table row, a card, a settings page) and
   put the command in `data-command` (or `data-ask-leo-command-value` for the Stimulus
   variant). Loop over records to generate one button per row
   (`data-command="Ask Leo to ... <%= user.email %> ..."`).
3. Pick inline script (one page, zero setup) or the Stimulus controller (used on
   several pages / Turbo-heavy app).
4. Style the button to match your app — the pattern has no styling requirements.
5. Safe to drop: `auto_send` (default false), the Stimulus variant, the per-row loop —
   the minimal version is one button and the five-line script.
