Quickstart.
Three examples, escalating in complexity. Each one is a complete,
runnable YAML manifest you can paste into the Bolt admin UI and save.
Together they walk every primitive the flow grammar has in v1:
call, parallel, transform,
and emit, plus list and card presentation.
What you'll write
A single YAML file per App. Bolt loads it at startup (or hot-reloads it from the admin UI). Each manifest declares four things:
- Identity. How the chip is named, versioned, and what type of row it emits.
- Scopes. Sub-commands the user can pick. The minimum is one.
- Flow. A list of declarative steps (
call,parallel,merge,transform,emit). - Presentation. How rows render in the launcher list and the right rail.
The three examples below show what changes as you add complexity. The first one is single-call. The second adds sequential dependency, parallel fan-out, and merge. The third is wide parallel with a multi-row board.
1 · =worldclock — the hello world
One public HTTP call, one row. The smallest useful manifest. Once this saves and runs, every richer one is incremental.
schema_version: 1
id: worldclock
chip: =worldclock
title: World Clock
description: Current time for an IANA timezone.
emits: Generic
scopes:
- id: lookup
label: Lookup zone
default: true
flow:
- call:
kind: http
method: GET
url: "https://timeapi.io/api/Time/current/zone"
query:
timeZone: "{{query | default:'America/New_York'}}"
out: r
- emit: r
presentation:
widget: list
title_field: dateTime
subtitle_field: timeZone
empty_state: "Try =worldclock America/New_York or Asia/Kolkata" What each block does
Identity. schema_version: 1 pins this manifest to the v1 parser.
id is the unique key used in audit logs and admin UI.
chip: =worldclock is what the user types in the launcher.
emits: Generic tells Bolt which canonical row type the
emitted rows conform to.
Scopes. Every manifest needs at least one scope. Mark
the entry scope default: true so it fires whenever the
user activates the chip. The user's text after the chip is exposed as
the template variable {{query}}; the rest of the flow
can reference it inline. Use match only when you need
pattern-based dispatch across multiple sub-flows; it takes a list of
regex: or text: branches. See
Scopes for the full grammar.
Flow.
A flow is a list of steps. call runs one tool. This call
is kind: http, method GET, against a public
endpoint. The out: r line names this call's output so the
next step can reference it. See
Flow DAG for
parallel, merge, and transform.
Emit.
The flow must end with exactly one emit: step. The value
is the name of a prior output (here, r), and the response
body becomes one row of the declared canonical type. For responses
that are arrays or nested objects, add a path: JSON
pointer (e.g. path: /events) to point at the array to
emit. The full binding grammar is in
Emit.
Presentation.
Optional but recommended. widget: list renders results
as a list. title_field and subtitle_field
name response fields to display per row. empty_state is
what the launcher shows when the chip has no input yet. The
Presentation
section covers the full list.
Try it
- Open the admin UI, go to Apps → Custom, click New manifest.
- Paste the YAML above. Save.
- Open the launcher. Type
=worldclock America/New_Yorkand press Enter. - You should see one row with the current time. The right rail shows the full timezone block.
2 · =country — multi-source synthesis
Five public APIs, one rich row. This is where Bolt earns the pitch: a real answer assembled from sources that no single API can give you alone, with typo tolerance built into the resolution step.
schema_version: 1
id: country
chip: =country
title: Country dossier
description: Brief on any country. Typo-tolerant lookup plus current local time, currency, language, GDP, and Wikipedia summary. Five public APIs, one row.
emits: Place
scopes:
- id: dossier
label: Country dossier
default: true
flow:
# 1. Open-Meteo Geocoding resolves the user's query (typo-tolerant) into
# a canonical name + ISO-2 country_code + lat/lon + IANA timezone.
- call:
kind: http
method: GET
url: 'https://geocoding-api.open-meteo.com/v1/search'
query:
name: '{{query | default:''japan''}}'
count: '1'
out: geo
# 2. REST Countries by ISO-2 alpha code: more reliable than /name/ for
# common-vs-official-name ambiguity. Returns a single-element array.
- call:
kind: http
method: GET
url: 'https://restcountries.com/v3.1/alpha/{{geo.results.0.country_code | default:''JP''}}'
query:
fields: 'cca3,name,flags,capital,population,region,subregion,languages,currencies,latlng'
out: base_arr
# 3. Normalize into a flat object. REST Countries returns an array for /name/
# but a single object for /alpha/{code}?fields=, so we handle both shapes.
- transform: 'if (.base_arr | type) == "array" then .base_arr[0] else .base_arr end'
out: base
# 4. Parallel fan-out: narrative (Wikipedia), economic (World Bank GDP),
# and current local time (timeapi.io). Independent reads, run concurrently.
# tolerate_failures lets a single dead branch leave its output empty
# instead of failing the whole flow. Combined with the ? operator on
# field access in the merge step, the row still renders.
- parallel:
- call:
kind: http
method: GET
url: 'https://en.wikipedia.org/api/rest_v1/page/summary/{{base.name.common | url_escape}}'
# Wikipedia requires a User-Agent per their robot policy.
# Other public APIs we hit do not strictly require it but it
# is good citizenship to identify your client.
headers:
User-Agent: 'Sparcle Bolt Apps Demo ([email protected])'
out: wiki
- call:
kind: http
method: GET
url: 'https://api.worldbank.org/v2/country/{{base.cca3 | url_escape}}/indicator/NY.GDP.MKTP.CD'
query:
format: 'json'
per_page: '1'
mrnev: '1'
out: gdp
- call:
kind: http
method: GET
url: 'https://timeapi.io/api/Time/current/zone'
query:
timeZone: '{{geo.results.0.timezone | default:''UTC''}}'
out: now
tolerate_failures: true
# 5. Merge into a single rich row. The ? operator suppresses errors on
# missing-path access, and // null gives a null fallback. Together
# they let a partial parallel result still produce a renderable row.
- transform: '[{cca3: (.base.cca3? // null), name: (.base.name? // null), flags: (.base.flags? // null), capital: (.base.capital? // null), population: (.base.population? // null), region: (.base.region? // null), subregion: (.base.subregion? // null), latlng: (.base.latlng? // null), timezone: (.geo.results[0].timezone? // null), local_time: (.now.dateTime? // null), currency: (.base.currencies? | keys? | .[0]? // null), language: (.base.languages? | [.[]]? | .[0]? // null), summary: (.wiki.extract? // null), gdp_trillions: ((((.gdp[1][0].value? // 0) / 1e12) * 100 | floor) / 100), gdp_year: (.gdp[1][0].date? // null)}]'
out: rows
- emit: rows
presentation:
widget: list
title_field: name.common
subtitle_field: capital.0
list_fields:
- name.common
- capital.0
- region
row_key_field: cca3
empty_state: "Try =country japan, =country brazil, or even =country japn"
right_widget:
kind: card
mode: selected_row
bind:
title: "{{row.name.common}}"
subtitle: "{{row.region}} · {{row.subregion}}"
image_url: "{{row.flags.png}}"
fields:
- { label: "Capital", value: "{{row.capital.0}}" }
- { label: "Local time", value: "{{row.local_time | date:'short'}}" }
- { label: "Timezone", value: "{{row.timezone}}" }
- { label: "Currency", value: "{{row.currency}}" }
- { label: "Language", value: "{{row.language}}" }
- { label: "Population", value: "{{row.population}}" }
- { label: "GDP (USD)", value: "${{row.gdp_trillions}}T ({{row.gdp_year}})" }
- { label: "Summary", value: "{{row.summary | truncate:280}}" } What this teaches that =worldclock didn't
Where =worldclock exercised one HTTP call and a list,
=country is the full flow grammar in a single manifest.
It chains two sequential calls so the second depends on the first's
output, unwraps an array with a transform, fans three
independent enrichments out in parallel, and merges them
with a jq one-liner into the single canonical row that the
card right_widget binds to. Five public APIs, one row,
one screen.
The runtime patterns map cleanly to the spec. The two leading calls
are textbook sequential dependency from
Flow DAG (section 6),
with the geocoder's {{geo.results.0.country_code}}
feeding the next URL. The fan-out uses parallel from
section 6.2;
the two transform nodes use jq exactly as
section 6.4
describes, with // null fallbacks so a missing upstream
field never crashes the row. The right rail is a
kind: card in selected_row mode per
section 11;
row_key_field: cca3 stabilizes selection across
re-renders.
Type =country brazil and you get the wow row: a flag,
the country name, "South America · South America" as the
subtitle, then a card with Brasília, the current Brasília
time, BRL, Portuguese, the population in millions, last-reported GDP
in trillions with the year, and a 280-character Wikipedia paragraph.
One launcher fire, one row, no tabs, no clicks. The same shape works
for Japan, Kenya, or any other country that has an entry in all five
public services.
Typo tolerance is free because Open-Meteo Geocoding is fuzzy on the
way in. Type =country japn and the geocoder resolves it
to Japan, the ISO-2 code JP flows into REST Countries,
and the rest of the pipeline never even sees the typo. There is no
spellcheck step in the manifest; the right upstream API is the
spellcheck step.
One trade-off worth naming: =country emits a single row
by design. The v1 flow grammar has no per-row fan-out primitive, so a
"give me a dossier for each of these 10 countries" pattern is not
expressible in one manifest. The right move there is a separate
utility for the multi-row case, or a kind: utility
action that re-fires =country for the selected name.
Try it
- Paste the YAML above into a new manifest. Save.
- Open the launcher. Type
=country japanand press Enter. - You should see one row with Japan's flag, capital, region, and population. The right rail card shows the full dossier with local time, currency, language, GDP, and Wikipedia summary.
- Try
=country japnto verify the typo tolerance.
3 · =status — wide parallel, multi-row board
Five Statuspage.io feeds in parallel, one five-row status board. The canonical pattern for any "multi-vendor unified view" where the upstream sources agree on shape.
# =status — SaaS vendor status board via public Statuspage.io feeds.
#
# Reference manifest for the WIDE PARALLEL FAN-OUT pattern. Five vendor
# status pages, all on the Statuspage.io platform, all expose the same
# /api/v2/summary.json schema. We fan out concurrently, shape each vendor
# into one row of a unified array via jq, and render a 5-row status board
# sorted so vendors with active incidents bubble to the top.
schema_version: 1
id: status
chip: =status
title: SaaS status board
description: Live status of vendors most teams depend on. Five public Statuspage.io feeds, one parallel fan-out.
emits: Generic
scopes:
- id: board
label: Status board
default: true
flow:
# 1. Fan out to 5 vendor Statuspage.io feeds concurrently.
# All return the same schema: {page, status, components, incidents}.
- parallel:
- call:
kind: http
method: GET
url: https://www.githubstatus.com/api/v2/summary.json
out: github
- call:
kind: http
method: GET
url: https://www.stripestatus.com/api/v2/summary.json
out: stripe
- call:
kind: http
method: GET
url: https://www.cloudflarestatus.com/api/v2/summary.json
out: cloudflare
- call:
kind: http
method: GET
url: https://discordstatus.com/api/v2/summary.json
out: discord
- call:
kind: http
method: GET
url: https://status.openai.com/api/v2/summary.json
out: openai
# 2. Shape each vendor into one row, then sort by active incidents desc
# so vendors with issues surface first.
- transform: |
[
{ vendor: "GitHub", indicator: .github.status.indicator, description: .github.status.description, incidents: (.github.incidents | length), url: .github.page.url },
{ vendor: "Stripe", indicator: .stripe.status.indicator, description: .stripe.status.description, incidents: (.stripe.incidents | length), url: .stripe.page.url },
{ vendor: "Cloudflare", indicator: .cloudflare.status.indicator, description: .cloudflare.status.description, incidents: (.cloudflare.incidents | length), url: .cloudflare.page.url },
{ vendor: "Discord", indicator: .discord.status.indicator, description: .discord.status.description, incidents: (.discord.incidents | length), url: .discord.page.url },
{ vendor: "OpenAI", indicator: .openai.status.indicator, description: .openai.status.description, incidents: (.openai.incidents | length), url: .openai.page.url }
] | sort_by(.incidents) | reverse
out: rows
# 3. Emit the unified 5-row board.
- emit: rows
actions:
- id: open
kind: url
target: row
label: "Open status page"
icon: external_link
url_template: '{{row.url}}'
presentation:
widget: list
title_field: vendor
subtitle_field: description
list_fields:
- vendor
- description
- incidents
row_key_field: vendor
empty_state: "Live status of GitHub, Stripe, Cloudflare, Discord, OpenAI"
right_widget:
kind: card
mode: selected_row
bind:
title: '{{row.vendor}}'
subtitle: '{{row.description}}'
fields:
- { label: "Status indicator", value: '{{row.indicator}}' }
- { label: "Open incidents", value: '{{row.incidents}}' }
- { label: "Status page", value: '{{row.url}}' }
actions:
- open What this teaches that the first two didn't
The third example, =status, demonstrates three primitives
the first two did not exercise together: wide parallel fan-out across
five concurrent HTTP branches, a multi-row unified list as output
(not just enrichment of a single row), and a row-targeted
url action that turns every list entry into a clickable
handoff. Where =country made calls one-at-a-time before
fanning, =status fans out from the first step.
The reason this manifest stays inside the v1 primitives is the
Statuspage.io schema. GitHub, Stripe, Cloudflare, Discord, and OpenAI
all publish their status pages on the same platform, and every
Statuspage.io tenant exposes /api/v2/summary.json with
the identical shape: page, status (with
indicator in the fixed set none/minor/major/critical),
components, and incidents. That schema
uniformity is the load-bearing assumption. It lets a single
transform step reach into all five branches with the
same path expression per vendor and emit a uniform row, without
needing the merge: primitive's per-source schema mapping.
The "wow row" is the sort. When everything is green the board shows
five "All Systems Operational" rows in a stable order. The moment
any one vendor starts an incident, sort_by(.incidents) | reverse
floats it to the top of the list, and the subtitle flips to something
like "Partial System Outage" or "Service Disruption." For an SRE or
platform on-call, that is the screenshot moment: open the launcher,
type =status, and the vendor that broke your morning is
already row one.
Treat this as the canonical pattern for any "multi-vendor unified
view" where the upstream sources agree on shape. Other examples that
fit the same mold: package-registry health across npm, PyPI,
crates.io, and RubyGems; CDN edge-status feeds; multi-region
cloud-provider status dashboards; news feeds that share an RSS or
JSON Feed schema. Anywhere the upstream APIs share a schema, you can
skip merge: entirely and let a single
transform stamp out a uniform row per branch.
The trade-off worth naming up front: the five vendors are hardcoded in the YAML. Adding a sixth means editing the manifest and reshipping, not flipping a config toggle. A future spec version could introduce a fan-out-over-list primitive that takes a vendor table as input, but v1 stays explicit on purpose. For the small, stable set of vendors a given team actually cares about, hardcoding is the right altitude.
Try it
- Paste the YAML above into a new manifest. Save.
- Open the launcher. Type
=statusand press Enter. - You should see five rows, one per vendor. Vendors with active incidents bubble to the top.
- Arrow-key through the rows to see each vendor's card in the right rail. Press Enter on a row to open its public status page in a new tab.
Where to go next
- Sample gallery. Real-world manifests for Jira, Salesforce, Slack, Notion, Linear, GitHub, and more. Fork one as a starting point for your own.
- Full manifest spec (v1). Every field, every step kind, every validation rule. The source of truth.
- Scopes. Add a second scope (e.g.
=acme myvs=acme search) and route between them withmatch:. - Merge. When upstream sources have different schemas (e.g. PagerDuty + Datadog + ServiceNow incidents), use the full
merge:primitive with per-source field mapping instead of a singletransform. - Actions. Add typed actions on rows (open in source, run a tool, pipe into the composer).
=statusabove shows the simplest pattern.
Stuck on something this page should have covered? Reach the team via the contact link in the footer. Manifest authoring questions get priority routing.