← Apps

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:

  1. Identity. How the chip is named, versioned, and what type of row it emits.
  2. Scopes. Sub-commands the user can pick. The minimum is one.
  3. Flow. A list of declarative steps (call, parallel, merge, transform, emit).
  4. 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

  1. Open the admin UI, go to Apps → Custom, click New manifest.
  2. Paste the YAML above. Save.
  3. Open the launcher. Type =worldclock America/New_York and press Enter.
  4. 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

  1. Paste the YAML above into a new manifest. Save.
  2. Open the launcher. Type =country japan and press Enter.
  3. 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.
  4. Try =country japn to 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

  1. Paste the YAML above into a new manifest. Save.
  2. Open the launcher. Type =status and press Enter.
  3. You should see five rows, one per vendor. Vendors with active incidents bubble to the top.
  4. 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 my vs =acme search) and route between them with match:.
  • 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 single transform.
  • Actions. Add typed actions on rows (open in source, run a tool, pipe into the composer). =status above 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.