Templates

These endpoints store and inspect templates: save one, list them, copy, rename, pick the default, move them between Macs as bundles, and render a preview image. They also serve the lookups a template author needs: the component schema, SF Symbol names and the installed Shortcuts. The examples use the $HERALD and $TOKEN variables from Connect.

What a template is#

A template is a saved layout for one app's banners. It is a grid of cells, and each cell holds a component such as text, an image or a row of buttons. A component is bound to a field of the notification, for example {title}, so one template draws every notification the app sends.

A template belongs to one app and has a name. A notification picks its template in this order:

  1. The template named in the notification.
  2. The defaultTemplate of the app's manifest.
  3. One of the four built-in layouts: builtin.imageLeft, builtin.imageRight, builtin.hero, builtin.compact.

You can design a template by hand in the Designer or send its JSON to this API. The fields of the template object are documented in Grid and layout; this page covers only the endpoints.

The Designer edits the same template object these endpoints store. A template saved here appears in the Designer at once.
The Designer window with a template open on the grid

The Designer edits the same template object these endpoints store. A template saved here appears in the Designer at once.

Endpoints#

EndpointPurpose
GET /v1/templatesList saved templates.
PUT /v1/templatesSave a template.
DELETE /v1/templatesDelete a template.
POST /v1/templates/duplicateCopy a template.
POST /v1/templates/renameRename a template.
PUT /v1/templates/defaultSet or clear an app's default template.
PUT /v1/templates/follow-upSet or switch off a template's follow-up.
GET /v1/templates/exportPack a template and its assets into a bundle.
POST /v1/templates/importInstall a bundle.
POST /v1/previewRender a template to a PNG.
GET /v1/previewRender a saved template with sample data.
GET /v1/componentsThe schema of every component.
GET /v1/symbolsSearch SF Symbol names.
GET /v1/shortcutsList the installed Apple Shortcuts.

Store templates#

GET /v1/templates#

Lists the templates saved for one app, or for every app. Each item is the full template object.

Request

NameInTypeRequiredDescription
appquerystringoptionalReturn only this app's templates. Default: every app.
Example requestshell
curl -s "$HERALD/v1/templates?app=example.bidbot" -H "Authorization: Bearer $TOKEN"
Example responseJSON
{
  "items": [
    {
      "name": "bid-won",
      "app": "example.bidbot",
      "layoutVersion": 2,
      "collapseEmpty": true,
      "grid": {
        "rows": 2,
        "cols": 2,
        "rowSizes": ["auto", "auto"],
        "colSizes": ["48", "fill"],
        "gap": 8,
        "padding": 14,
        "width": 400
      },
      "cells": [
        {"id": "icon", "row": 0, "col": 0, "rowSpan": 2, "component": {"type": "issuerIcon"}},
        {"id": "title", "row": 0, "col": 1, "component": {"type": "text", "binding": "{title}", "style": "title"}},
        {"id": "body", "row": 1, "col": 1, "component": {"type": "text", "binding": "{body}", "style": "body"}}
      ]
    }
  ]
}

Notes

  • The four builtin.* layouts are not listed. They always exist.
  • Templates whose names start with _ are scratch templates, such as the one the Designer writes for Send test. They work by name and are never listed.

PUT /v1/templates#

Saves a template. If the app already has a template with the same name, it is replaced. The template is validated before it is stored, so a bad layout is refused here and not when a notification arrives.

Request

The body is one template object. These two fields are required; the rest are in Grid and layout.

NameInTypeRequiredDescription
namebodystringrequiredThe template's name.
appbodystringrequiredThe app the template belongs to.
gridbodyobjectoptionalThe rows, columns and sizes of the grid.
cellsbodyarrayoptionalThe cells and the component in each.
Example requestshell
curl -s -X PUT "$HERALD/v1/templates" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "bid-won",
    "app": "example.bidbot",
    "layoutVersion": 2,
    "collapseEmpty": true,
    "grid": {"rows": 2, "cols": 2, "rowSizes": ["auto", "auto"], "colSizes": ["48", "fill"],
             "gap": 8, "padding": 14, "width": 400},
    "cells": [
      {"id": "icon", "row": 0, "col": 0, "rowSpan": 2, "component": {"type": "issuerIcon"}},
      {"id": "title", "row": 0, "col": 1,
       "component": {"type": "text", "binding": "{title}", "style": "title"}},
      {"id": "body", "row": 1, "col": 1,
       "component": {"type": "text", "binding": "{body}", "style": "body", "maxLines": 3}}
    ]
  }'
Example responseJSON
{"ok": true}

Errors

StatusWhen
400name or app is missing.
400The template is invalid. The message lists every problem with its location.
500The file could not be written.

Notes

  • A validation message names the cell and the path of each problem, separated by semicolons, for example invalid template: grid.rowSizes: rowSizes has 1 entries but rows is 2; grid.gap: gap must be 0 to 64. A problem inside a cell starts with cell <id>:.
  • Templates are files: templates/<app>/<name>.json in Herald's support folder.

DELETE /v1/templates#

Deletes one saved template.

Request

NameInTypeRequiredDescription
appquerystringrequiredThe app the template belongs to.
namequerystringrequiredThe template's name.
Example requestshell
curl -s -X DELETE "$HERALD/v1/templates?app=example.bidbot&name=bid-won" \
  -H "Authorization: Bearer $TOKEN"
Example responseJSON
{"ok": true}

Errors

StatusWhen
400app or name is missing.
404The app has no template with this name.

Copy, rename and choose the default#

POST /v1/templates/duplicate#

Copies a saved template or a built-in layout. Use it to start a new design from an existing one, or to give another app the same layout.

Request

NameInTypeRequiredDescription
appbodystringrequiredThe app that owns the template to copy.
namebodystringrequiredThe template to copy: a saved name or a builtin.* name.
newNamebodystringoptionalThe name of the copy. Default: the old name followed by copy.
toAppbodystringoptionalThe app that receives the copy. Default: the same app.
Example requestshell
curl -s -X POST "$HERALD/v1/templates/duplicate" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"app": "example.bidbot", "name": "builtin.hero", "newName": "bid-hero"}'
Example responseJSON
{
  "ok": true,
  "app": "example.bidbot",
  "name": "bid-hero",
  "copiedFrom": {"app": "example.bidbot", "name": "builtin.hero"}
}

Errors

StatusWhen
400newName is not a valid name. See the notes.
404The template to copy does not exist.
409A template named newName already exists for the receiving app.

Notes

  • Without newName the copy is named after the original: bid-won copy, then bid-won copy 2 when that is taken.
  • A template name cannot be empty, contain / or :, start with . or _, start with builtin., or be longer than 128 bytes.

POST /v1/templates/rename#

Renames a saved template. If it was the app's default template, the manifest follows the new name.

Request

NameInTypeRequiredDescription
appbodystringrequiredThe app that owns the template.
namebodystringrequiredThe current name.
newNamebodystringrequiredThe new name.
Example requestshell
curl -s -X POST "$HERALD/v1/templates/rename" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"app": "example.bidbot", "name": "bid-hero", "newName": "bid-accepted"}'
Example responseJSON
{
  "ok": true,
  "app": "example.bidbot",
  "name": "bid-accepted",
  "renamedFrom": "bid-hero",
  "isDefault": false,
  "note": "Command, script and Shortcut approvals belong to the old name; the user is asked again the first time they run."
}

Errors

StatusWhen
400name is a builtin.* layout, or newName is not a valid name.
404The template does not exist.
409A template named newName already exists for the app.

Notes

  • Approvals to run commands, scripts and Shortcuts are tied to the template's name. After a rename the user is asked again the first time one runs.
  • Built-in layouts cannot be renamed. Duplicate one instead.

PUT /v1/templates/default#

Sets the template an app uses when a notification names none, or clears it. The default is stored in the app's manifest as defaultTemplate, so the app must have a manifest.

Request

NameInTypeRequiredDescription
appbodystringrequiredThe app to change.
namebodystringoptionalA saved template or a builtin.* layout. Omit it to clear the default.
Example requestshell
curl -s -X PUT "$HERALD/v1/templates/default" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"app": "example.bidbot", "name": "bid-accepted"}'
Example responseJSON
{"ok": true, "app": "example.bidbot", "defaultTemplate": "bid-accepted"}

Errors

StatusWhen
400The app has no manifest. Save one with PUT /v1/manifest first.
404name is not a saved template or built-in layout.

PUT /v1/templates/follow-up#

Sets the follow-up of a template, or switches it off. A follow-up runs one action when a banner is left unattended (see Follow-ups). The route edits the template for you, so you do not read and rewrite it. It never approves code. When the action runs code, the result says the person still has to approve it at the Mac.

Request

NameInTypeRequiredDescription
appbodystringrequiredThe app that owns the template.
templatebodystringoptionalThe template to edit. Default: the app's default template, which Herald creates from the current layout when the app has none.
afterbodynumber or stringoptionalSeconds from 5 to 604800, or "90s", "10m", "2h". Required unless enabled is false.
shortcutbodystringoptionalRun this Apple Shortcut.
scriptbodystringoptionalRun this file from Herald's scripts folder.
commandbodystringoptionalRun this shell command.
actionRefbodystringoptionalRun the action of this id, or label, that the notification offers.
inputbodystringoptionalText for the Shortcut or script, with {tokens} filled.
labelbodystringoptionalThe name shown in Follow-up ran: LABEL. Default: the Shortcut or script name, or Follow-up for a command.
enabledbodybooleanoptionalfalse switches the follow-up off. Default true.

Give exactly one of shortcut, script, command and actionRef, unless enabled is false.

Example requestshell
curl -s -X PUT "$HERALD/v1/templates/follow-up" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"app": "example.bidbot", "template": "Bid won", "after": "10m",
       "shortcut": "Forward to phone", "input": "{title}"}'
Example responseJSON
{
  "saved": true,
  "app": "example.bidbot",
  "template": "Bid won",
  "createdTemplate": false,
  "followUp": {"after": 600, "action": {"id": "follow-up", "label": "Forward to phone",
               "kind": "shortcut", "shortcut": "Forward to phone", "input": "{title}"}},
  "action": {"id": "follow-up", "label": "Forward to phone",
             "kind": "shortcut", "shortcut": "Forward to phone", "input": "{title}"},
  "origin": "template",
  "approval": "needs-approval",
  "needsApproval": true,
  "note": "Approval stays with the person at the Mac: the banner asks the first time the action would run, and Always allow lets later follow-ups run unattended. Nothing was approved here. list_shortcuts names the installed Shortcuts."
}

Response fields

FieldTypeDescription
savedbooleantrue when the template was written.
app, templatestringThe app and the template that now carries the follow-up.
createdTemplatebooleantrue when Herald made the template because the app had none.
manifestCreatedbooleanWith createdTemplate: true when Herald also made a manifest for the app.
defaultTemplatestringWith createdTemplate: the new template, now the app's default.
followUpobjectThe stored follow-up, with after in seconds.
actionobjectThe action it runs, as stored. null when the follow-up is off.
originstringtemplate or issuer. It decides which approval applies. null when the follow-up is off.
approvalstringapproved, needs-approval, app-permission-needed, app-not-allowed or none. See below.
needsApprovalbooleantrue when nothing runs until the person approves.
notestringA sentence that says what happens next.

The approval values:

ValueMeaning
approvedThe person already approved this template's code. The follow-up runs.
needs-approvalThe person has to approve the template's code, in the banner's question, the first time.
app-permission-neededThe action is an issuer action. The app asked to run commands, scripts and Shortcuts and the person has not allowed it yet: the banner asks the first time it would run.
app-not-allowedThe action is an issuer action and the app did not register with allowCommands, so it cannot run.
noneThe action runs no code, such as a callback to this Mac, or the follow-up is off.

Errors

StatusWhen
400after is missing or outside 5 to 604800 seconds, or is not a duration.
400No action, or more than one, is named; input goes with a command or actionRef; or script is a path.
400The kind cannot follow up, or actionRef names an action the app does not offer.
400enabled is false together with another follow-up field, or template names a built-in layout.
404The template named does not exist.

Notes

  • Priority is template, then notification, then manifest. A template follow-up wins over the issuer's.
  • "enabled": false also switches off a follow-up that the issuer declares.
  • With actionRef, the action may be one the issuer offers. It then runs under the app's permission and origin is issuer.
  • Approval is bound to the template's name and its code. Changing the Shortcut asks again.
  • A built-in layout is read-only. Duplicate it first, or leave out template to use the app's default.

Move templates between Macs#

A bundle is one .heraldtemplate file that holds a template and the Rive animations it uses. Export a bundle to share a design, and import it on another Mac or into another app. How bundles treat assets is explained in Rive: packaging with a template.

GET /v1/templates/export#

Packs a template and its Rive files into a bundle. Herald either writes the bundle to a path you give, or returns it in the reply as base64.

Request

NameInTypeRequiredDescription
appquerystringrequiredThe app that owns the template.
namequerystringrequiredA saved template or a builtin.* layout.
pathquerystringoptionalWhere to write the file. Must end in .heraldtemplate.
Example requestshell
curl -s -G "$HERALD/v1/templates/export" -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "app=example.bidbot" --data-urlencode "name=bid-accepted" \
  --data-urlencode "path=$HOME/Desktop/bid-accepted.heraldtemplate"
Example responseJSON
{
  "ok": true,
  "app": "example.bidbot",
  "name": "bid-accepted",
  "file": "bid-accepted.heraldtemplate",
  "bytes": 18432,
  "assets": ["confetti.riv"],
  "warnings": [],
  "path": "/Users/you/Desktop/bid-accepted.heraldtemplate"
}

Response fields

FieldTypeDescription
filestringThe suggested file name of the bundle.
bytesintegerThe size of the bundle.
assetsarrayThe Rive files packed into it.
warningsarrayProblems that did not stop the export, such as a referenced file that is missing.
pathstringWhere the file was written. Present when you sent path.
base64stringThe bundle itself. Present when you did not send path.

Errors

StatusWhen
400path does not end in .heraldtemplate, or its folder does not exist.
404The template does not exist.

POST /v1/templates/import#

Installs a bundle: saves its template and stores its Rive files. You choose what happens when a template with the same name already exists.

Request

Give exactly one of base64 and path.

NameInTypeRequiredDescription
pathbodystringoptionalA .heraldtemplate file on this Mac.
base64bodystringoptionalThe bundle as base64, for small bundles.
appbodystringoptionalInstall the template for this app. Default: the app named in the bundle.
onConflictbodystringoptionalkeepBoth, replace or fail. Default keepBoth.
Example requestshell
curl -s -X POST "$HERALD/v1/templates/import" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"path": "~/Desktop/bid-accepted.heraldtemplate", "app": "example.bidbot", "onConflict": "keepBoth"}'
Example responseJSON
{
  "ok": true,
  "app": "example.bidbot",
  "name": "bid-accepted 2",
  "renamedFrom": "bid-accepted",
  "replaced": false,
  "installedAssets": ["confetti.riv"],
  "reusedAssets": [],
  "missingAssets": [],
  "warnings": []
}

Response fields

FieldTypeDescription
namestringThe name the template was saved under.
renamedFromstringThe name in the bundle, when keepBoth had to pick a new one.
replacedbooleantrue when an existing template was overwritten.
installedAssetsarrayRive files copied into the app's assets.
reusedAssetsarrayRive files the app already had, left as they were.
missingAssetsarrayFiles the template refers to that were not in the bundle.
warningsarrayProblems that did not stop the import.

Errors

StatusWhen
400Both or neither of base64 and path were sent, or the bundle cannot be read.
400onConflict is not one of the three values.
409onConflict is fail and a template with that name exists.
413The bundle is too large.

Notes

  • keepBoth saves the import under a new numbered name. replace overwrites. fail refuses.
  • A request body is limited to 1 MB, so use path for any bundle with animations in it.

Preview a template#

POST /v1/preview#

Renders a template to a PNG image with the same code that draws real banners, without showing anything on screen. Use it to check a design, including how it looks with long text, missing fields, in dark mode or as a stack.

Request

NameInTypeRequiredDescription
appbodystringrequiredThe app. Optional when template is an object that names its app.
templatebodystring or objectoptionalA template name, or a whole template object.
databodystring or objectoptionalThe notification to draw: "sample", "last" or an object. Default "sample".
appearancebodystringoptionallight or dark. Default light.
scalebodynumberoptionalPixel scale from 1 to 3. Default 2.
stackCountbodyintegeroptionalFrom 1 to 99. Above 1, draws the banner as the top card of a stack. Default 1.
stackExpandedbodybooleanoptionalWith stackCount above 1, draws the stack open as a list.
confirmationbodystring or objectoptionalDraws an inline question on the banner. See the notes.
replyingbodybooleanoptionaltrue draws the reply field in place of the buttons.
replySamplebodystringoptionalThe text shown in the reply field when replying is true.
Example requestshell
curl -s -X POST "$HERALD/v1/preview" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "app": "example.bidbot",
    "template": "bid-accepted",
    "data": {"title": "Bid accepted", "body": "Acme accepted your bid of $4,200."},
    "appearance": "dark"
  }' -o preview.png

Example response

The reply is the image itself, with Content-Type: image/png. The command above saves it as preview.png.

text
preview.png: PNG image data, 800 x 196, 8-bit/color RGBA

Errors

StatusWhen
400app is missing, or a value is invalid. The message names the field.
400An inline template is invalid. The message names the cell.
400data is "last" and the app has no notification in History.
500The render failed, for example because the named template does not exist.

Notes

  • Without template, Herald uses the app's default template, or builtin.imageLeft.
  • data: "sample" uses the sample values of the app's manifest. data: "last" uses the app's most recent notification. An object is read like the body of POST /v1/notify: leave a field out to see how the template collapses without it.
  • A preview cannot play a rive component and draws a labelled placeholder in its place. Menus are drawn as static labels, and symbol effects do not animate.

Confirmation kinds#

Herald sometimes asks a question inside a banner before it acts, for example before it runs a command for the first time. confirmation draws such a question so that you can check how it looks. Send a kind as a string, or an object with a kind and optional text.

KindThe question it draws
callbackHostWhether to let the app call a server on another machine.
commandWhether to run a shell command for the app.
scriptWhether to run a script for the app.
shortcutWhether to run an Apple Shortcut for the app.
templateCommandWhether to run a command that a template added.
destructiveActionWhether to go ahead with a destructive button.
connectorConsentWhether to let a cloud connector send notifications.
remindersErrorA notice that adding to Reminders failed.
remindersDeniedThe same notice, with a button to open System Settings.

The object form accepts these optional strings to fill in the question's text. Each has a sample default.

FieldUsed byDescription
nameAll kindsThe app's name in the question. Default: the app id.
host, urlcallbackHost, connectorConsentThe host, and the full address, being asked about.
commandcommand, script, shortcut, templateCommandThe text of what would run.
template, others, replacestemplateCommandThe template's name, its other commands, and the app button it replaces.
labeldestructiveActionThe label of the destructive button.
codeconnectorConsentThe code a connector without a browser printed.
messageremindersError, remindersDeniedThe error text.
JSON
{
  "app": "example.bidbot",
  "template": "bid-accepted",
  "confirmation": {"kind": "command", "command": "open -a Numbers ~/Bids/acme.numbers"}
}

GET /v1/preview#

The quick form of the preview: renders a saved template with the manifest's sample data. It is convenient in a browser-free check such as curl -o, because everything is in the URL.

Request

NameInTypeRequiredDescription
appquerystringrequiredThe app.
templatequerystringoptionalA saved template or builtin.* name. Default: the app's default template.
appearancequerystringoptionallight or dark. Default light.
scalequerynumberoptionalPixel scale from 1 to 3. Default 2.
stackCountqueryintegeroptionalFrom 1 to 99. Default 1.
stackExpandedquerybooleanoptionaltrue draws the stack open.
Example requestshell
curl -s "$HERALD/v1/preview?app=example.bidbot&template=bid-accepted&appearance=dark" \
  -H "Authorization: Bearer $TOKEN" -o preview.png

Example response

The reply is a PNG image, as for POST /v1/preview.

text
preview.png: PNG image data, 800 x 196, 8-bit/color RGBA

Errors

StatusWhen
400app is missing, or appearance, scale or stackCount is invalid.
500The render failed.

Look things up#

GET /v1/components#

Returns the component schema: every component type with its properties, allowed values and defaults, the binding rules, the action kinds, and worked examples. It is static data made for programs and agents that write templates. The human-readable version is the components reference.

Request

No parameters.

Example requestshell
curl -s "$HERALD/v1/components" -H "Authorization: Bearer $TOKEN"

Example response

The document is long. This is its outline, with the content of each part left out and the description shortened.

JSON
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Herald banner template, layoutVersion 2",
  "schemaVersion": 2,
  "description": "A Herald banner is drawn from a GRID.",
  "type": "object",
  "required": ["name", "app", "layoutVersion", "grid", "cells"],
  "properties": {},
  "definitions": {},
  "components": {},
  "bindings": {},
  "actions": {},
  "examples": [],
  "workflow": []
}

Response fields

FieldTypeDescription
propertiesobjectThe fields of the template object.
definitionsobjectThe shared types: grid, cell, size, alignment.
componentsobjectOne entry per component type with its properties and defaults.
bindingsobjectThe tokens a component can bind and how they are formatted.
actionsobjectThe action kinds and the rules a template can apply to them.
examplesarrayComplete templates that validate.
workflowarrayThe order of calls an agent should follow to design a template.

GET /v1/symbols#

Searches the SF Symbol names available on this Mac. Use a returned name as a component's symbol value. See the SF Symbols reference.

Request

NameInTypeRequiredDescription
qquerystringoptionalSearch words. Every word must match the name, Apple's search terms or a synonym.
categoryquerystringoptionalA category key from the categories list in the reply.
limitqueryintegeroptionalThe most names to return, up to 1000. Default 100.
offsetqueryintegeroptionalHow many matches to skip, for paging. Default 0.
Example requestshell
curl -s "$HERALD/v1/symbols?q=bell&limit=3" -H "Authorization: Bearer $TOKEN"
Example responseJSON
{
  "total": 28,
  "offset": 0,
  "limit": 3,
  "symbols": [
    {"name": "bell", "categories": ["multicolor", "objectsandtools"]},
    {"name": "bell.fill", "categories": ["multicolor", "objectsandtools"]},
    {"name": "bell.circle", "categories": ["multicolor", "objectsandtools", "variable"]}
  ],
  "categories": [
    {"key": "all", "title": "All", "icon": "square.grid.2x2", "count": 7779},
    {"key": "communication", "title": "Communication", "icon": "message", "count": 222}
  ]
}

Response fields

FieldTypeDescription
totalintegerHow many symbols match.
symbolsarrayThe matches on this page: name and the categories it belongs to.
categoriesarrayEvery category: key, title, icon and count.

Errors

StatusWhen
400category is not a known key. The message lists the known keys.
501This Mac has no SF Symbols list.

GET /v1/shortcuts#

Lists the names of the Apple Shortcuts installed on this Mac. Use a name in a template action of kind shortcut.

Request

No parameters.

Example requestshell
curl -s "$HERALD/v1/shortcuts" -H "Authorization: Bearer $TOKEN"
Example responseJSON
{"items": ["Create follow-up", "Log Entry"]}

Errors

StatusWhen
502The system shortcuts tool failed or is missing.
504The system shortcuts tool did not answer in time.

Edit this page on GitHub

Esc
Getting started
Guides