Appearance
Embed buttons
An embed button is a workflow trigger that the Kadmo extension places on a web page. A workflow gets one by carrying an embed block. Clicking the button collects values from the page, maps them to the workflow's parameters, runs the workflow on the local Kadmo runtime and shows the result next to the button.
How it works
| Stage | What happens |
|---|---|
| 1. Page loads | The extension takes the page's host name (for example github.com) and asks the runtime for embed workflows: GET http://localhost:9876/api/embed-workflows?domain=<host>. |
| 2. Runtime answers | The runtime reloads its workflows and returns every workflow that has an embed block: its id, title, embed, params and policies. It does not filter by domain; the parameter is ignored. |
| 3. Extension filters | The extension keeps the workflows whose policies.allowed_domains and embed.org / embed.repo match the page (see Which pages get a button). |
| 4. Buttons are placed | For each remaining workflow, a button is added at every visible element that matches embed.selector, or in a floating dock when there is no selector. A page watcher repeats this when the page changes (debounced by 500 ms), so content that loads later also gets buttons. |
| 5. Click | The extension builds the page context, maps it through param_map and calls POST /api/workflows/<id>/run?sync=true with the resulting params. |
| 6. Result | The button shows "Running..." until the run ends, then the result is shown according to embed.result. |
The runtime scans three places for workflows: your actions in ~/.kadmo/actions, published workflows in ~/.kadmo/workflows, and the workflows of installed skill packs (~/.kadmo/skills). A fresh install has none, so no buttons appear until you add a workflow with an embed block. The runtime passes the embed block to the extension unchanged; all the fields below are read by the extension.
Example
yaml
id: issue_brief
title: Brief this issue
params:
org: string
repo: string
issue_title: string
issue_body: string
focus: string
policies:
allowed_domains:
- github.com
embed:
selector: ".issue-header-actions" # one button per visible match
position: prepend
when: ".issue-body" # only on pages that have this element
label: "Brief"
org: my-org # first path segment must be my-org
extract:
title_text:
selector: ".issue-title"
attribute: textContent
body_text:
selector: ".issue-body"
attribute: innerText
prompt:
param: focus
title: "What should the brief focus on?"
placeholder: "Risks, open questions..."
param_map:
org: "{{_path.0}}"
repo: "{{_path.1}}"
issue_title: "{{title_text}}"
issue_body: "{{body_text}}"
focus: "{{focus}}"
result:
action: bubble
steps:
- type: llm.generate
prompt: |
Write a five-line brief of this issue in {{org}}/{{repo}}.
Focus: {{focus}}
Title: {{issue_title}}
Body: {{issue_body}}
as: brief
- type: control.stop
message: "{{brief}}"The selectors are placeholders; use selectors from the page you target. The text shown in the result is the run's output message, which a control.stop step's message sets (see Step types).
Field reference
embed
| Field | Type | Effect |
|---|---|---|
selector | CSS selector | Elements to attach a button to. Every match that is visible on the page gets its own button. Without selector, the button goes into a floating dock fixed to the right edge of the window. |
position | before, after, prepend, append | Where the button container goes relative to each target: before or after the element, or inside it at the start or end. Default append; any other value is treated as append. Several buttons at the same target and position share one container and stack vertically. |
when | CSS selector | The button is placed only if this selector matches something on the page at the time of placing. |
label | string | Button text. Default: the workflow title. With prompt set, an ellipsis (…) is added. The button's tooltip is always the workflow title. |
org | string | The page URL's first path segment must equal this (case-insensitive). Empty or absent matches all. |
repo | string | The page URL's second path segment must equal this (case-insensitive). Empty or absent matches all. |
parent_filter | object | Place the button only at targets whose ancestor passes a test. See parent_filter. |
extract | map of name to entry | Values read from the page at click time. See extract. |
param_map | map of param name to value | How the workflow's parameters are filled. See param_map. |
prompt | object | Ask for free text before the run. See prompt. |
result | object | What to do with the result. See result. |
extract
Each key under extract names a value; the value is added to the page context under that key when the button is clicked.
| Field | Effect |
|---|---|
selector | Element to read. self, or no selector, reads the button's target element. A selector that starts with # or contains a space is looked up in the whole page; any other selector is looked up inside the target first, then in the whole page. On a floating button (no target), self finds nothing. |
attribute | What to read: textContent or innerText (both trimmed), location.href or location.pathname (the page URL, read only if selector finds an element), or any other name, read as that HTML attribute (href, data-id, ...). Set it on every entry; without it nothing is read. |
pattern | Regular expression applied to the value. The first capture group is used, or the whole match if there is no group. If the pattern does not match, the full value is kept unchanged. |
multiple | true reads every element in the whole page that matches selector and joins the non-empty values. pattern is not applied in this mode. |
separator | Join string for multiple. Default: a newline. |
An entry that finds no element or reads an empty value adds nothing to the context.
parent_filter
| Field | Effect |
|---|---|
selector | The target's nearest ancestor (or the target itself) that matches this selector. Without one, no button is placed at that target. |
match.child_selector | Optional: test this element inside the ancestor instead of the ancestor itself. Without a match, no button. |
match.attribute | What to read from the tested element, as in extract (textContent, innerText or an attribute name). |
match.equals | The value must equal this exactly. Checked first if both are set. |
match.contains | The value must contain this. |
Without match, the ancestor only has to exist.
prompt
| Field | Effect |
|---|---|
param | Context key that receives the typed text. Map it in param_map, for example focus: "{{focus}}". |
title | Heading of the input box. Default: the button label. |
placeholder | Placeholder text. Default: "Add instructions...". |
A click opens a small box with a text area marked "Optional" and a Run button. Ctrl+Enter or Cmd+Enter also runs; Escape or the close button cancels without running. Empty text is allowed and is passed as an empty string.
result
action | Behaviour |
|---|---|
bubble (default) | Show the output message as plain text in a box under the button, titled "Summary". It closes with its close button or a click elsewhere. |
type | Write the output message into the element at result.selector: an editable area or input is cleared and filled, with input events fired so the page notices. If the element is not found or writing fails, the bubble is shown instead. |
none | Show nothing; the button reads "Done" for two seconds. Use it when the workflow produces its own output. |
Errors are always shown in a bubble titled "Error". A run that succeeds without an output message also shows "Done" for two seconds.
Page context and param_map
At click time the extension builds a context from these keys, plus every extract key that produced a value and the prompt.param key:
| Key | Value |
|---|---|
_url | Full page URL |
_domain | Host name, for example github.com |
_pathname | URL path, for example /my-org/app/issues/42 |
_title | Page title |
_path.N | Path segment N, counting from 0: for the path above, _path.0 is my-org and _path.3 is 42 |
param_map is keyed by workflow parameter name. Each value is either:
- a template that is exactly
{{key}}, which takes the context value ofkey(if the key has no value, the parameter is left out of the run); or - anything else, passed through as a literal string. Mixed text such as
"Issue {{_path.3}}"is not filled in; it is sent as written.
Keep extract keys free of dots: a dotted key is read only as name.index into a list such as _path.
Every parameter listed under the workflow's params needs an entry in param_map, and a template entry must point at a built-in key, an extract key or the prompt.param key. If any parameter is unmapped, the extension places no button for that workflow, and logs nothing about it. When there is no param_map at all and the workflow has no params, the whole context is sent as parameters.
Which pages get a button
| Check | Rule |
|---|---|
policies.allowed_domains | If the list is not empty, the page's host must equal an entry or be a subdomain of it. github.com also matches gist.github.com; *.github.com matches github.com and its subdomains. If the list is empty or absent, every site matches. |
embed.org / embed.repo | Compared with the first and second URL path segments, as described above. |
when | Must match an element when the buttons are placed. |
selector | Must match at least one visible element (unless omitted, which uses the dock). |
parent_filter | Must pass for the target. |
| Connection | Buttons are placed only while the extension is connected to the runtime. Browser pages such as chrome:// and the Chrome Web Store never get buttons. |
What a click runs
The run goes through the same endpoint as any other run, and it is saved to run history like other runs. The run log records it as triggered by the dashboard. The extension waits for the run to finish; there is no time limit on the wait.
Browser steps in the workflow run in the tab the extension is attached to (see Browser extension), which may not be the tab where you clicked. Pass the page's data in through extract and param_map rather than reading it again with browser steps. If the workflow has browser steps and no tab is attached, the run is refused and the bubble shows an error that contains Browser not connected.
The Embed Buttons switch and privacy
The extension popup has a switch labelled Embed Buttons, described as "Show action buttons on sites". It is on by default.
- On: for each site you open, the extension sends the site's host name to the local runtime at
http://localhost:9876to ask for buttons. The request does not leave your computer. Pages from the extension's restricted list (browser pages, the Chrome Web Store) are skipped. - Off: no host names are sent, and buttons already on open pages are hidden in every tab. When you switch it back on, those buttons reappear; pages loaded while the switch was off get buttons after a reload.
Known issue: the switch is restored from browser storage after the extension's background worker starts. A page that wakes the worker can be handled before that, so with the switch off its host name may still be sent once and its buttons shown. Also, on a page that keeps changing, the page watcher can add new buttons after the switch was turned off; reload the page to clear them.
When changes show up
The extension asks once per host name per connection to the runtime, and caches the answer, including an empty one. Reloading a page on the same host reuses the cached list, so a workflow you add or edit does not appear until the cache is cleared. That happens whenever the extension reconnects to the runtime (for example after you restart kadmo serve, see CLI) and whenever Chrome restarts the extension's background worker. If the runtime is not running, nothing is cached and the next page load asks again. The runtime address is fixed at port 9876, so only the runtime that holds that port provides buttons, and only kadmo serve answers the request: a kadmo mcp started by an MCP client serves no /api, so no buttons appear while it holds the port.
When the connection drops, the active tab hides its buttons. Other open tabs keep theirs, and a click there shows the error "App not connected".
Editing in the dashboard
The dashboard marks actions that have an embed block or a non-empty allowed_domains list with an "Embeddable" badge, and shows an "Embedded Button" section on the action's page. Its editor is limited:
- It saves only
selector,position,labeland a "URL Pattern" field. Saving replaces the wholeembedblock, soextract,param_map,prompt,result,org,repoandparent_filterare lost. - It offers the positions Before, After and Inside. The extension treats "Inside" as
append, and nothing reads the URL pattern. - It needs a selector, so floating buttons cannot be made there.
Edit the YAML file for anything beyond a simple button. The look of the buttons is fixed and cannot be styled.
Troubleshooting
| Symptom | Check |
|---|---|
| No button | The Embed Buttons switch is on; the extension is connected; allowed_domains, org, repo, when and selector match the page; every workflow parameter has a valid param_map entry; the workflow loads: it is listed by kadmo list (see CLI). |
| A new or changed workflow has no button | The host's list is cached. Restart the runtime, then reload the page. |
| A parameter is empty | The extract entry has an attribute; its selector finds the element at click time; the param_map value is exactly {{key}}. |
pattern seems ignored | When the pattern does not match, the full value is kept. With multiple: true the pattern is not applied. |
| Button reads "Done" but shows no text | The workflow set no output message; end it with control.stop and a message. |
Related
- Workflow DSL: the rest of the workflow format
- Variables: how parameters are used in steps
- Browser extension: connecting the extension and the popup