Highport orbital control

Parameters

Every parameter you declare becomes a row on somebody's form. Orbital Control renders exactly what you declared, so the description you write is the only help they get.

A parameter is a named, typed blank in your tile that the person binding it fills in. You declare them in params on your space.highport.sites.tile record. Whoever binds the tile supplies values under the same names in their site record's parameters map. Your files read the resolved result at /_bard/params.json. The rest of this page is the detail of those three steps.

A tile may declare at most 32 parameters, and no two may share a name.

The declaration

Each entry of params is a space.highport.sites.tile#param. Four fields apply to every type. Everything else is a constraint, and each constraint belongs to one type only.

Field Required Bound What it is
name yes 64 bytes The key the site record supplies a value under, and the name your tile reads. Must match [A-Za-z][A-Za-z0-9_]*
type yes 317 bytes One of string, integer, boolean, blob, array, or an NSID naming a record type
required no — Defaults to false. Absent means optional
description no 1000 bytes The help text on the site owner's form

A parameter name may not begin with _. That prefix is reserved, because parameters are materialized under /_bard/params/.

A tile with three parameters: one required record, one optional string, one optional boolean.

"params": [
        { "name": "profile", "type": "app.bsky.actor.profile", "required": true,
          "description": "The profile record that is rendered" },
        { "name": "heading", "type": "string", "maxLength": 240, "maxGraphemes": 60,
          "description": "Shown above the profile. Defaults to the display name." },
        { "name": "showReplies", "type": "boolean",
          "description": "Show recent replies under the profile. Off by default." }
      ]
      

The types

Six kinds, and what each one means at both ends.

type The site owner supplies Your tile receives in /_bard/params.json
string Text they type, or a choice from a menu A JSON string
integer A whole number A JSON number. There are no floats in the data model at all, so a ratio is a string or a basis-point integer
boolean A checkbox true or false
blob A file, uploaded to their own account The blob reference, not a URL. The bytes are at a separate path
array Repeatable rows of whatever items declares A JSON array
an NSID One of their own records, pinned by reference or typed inline The resolved record object, $type included, however it was supplied

A parameter the site owner did not supply is absent from /_bard/params.json entirely. It is not null and not an empty string.

Blob parameters

This is the one that catches people. A blob parameter arrives in params.json as the blob reference the record carried, and a blob reference is not fetchable. Read params.json to learn whether the owner supplied a file. The bytes are a separate entry at /_bard/params/{name}{pointer}, where pointer is the RFC 6901 JSON Pointer to the blob inside the resolved value, and is empty when the parameter is the blob.

The declaration side needs nothing more. Building a tile has the paths and how to read them. On this page a blob is a type with two constraints, accept and maxSize.

Array parameters

An array must declare items, and items is a #itemSchema: a type plus the same constraint fields, with no name, required, description or nested items of its own. An array is one level deep and an item may never itself be an array.

Two item types have no working control on the site owner's form, so do not declare them: an array whose items.type is blob, and an array whose items.type is an NSID. The field renders, the owner fills something in, and the record is then refused with ParameterTypeMismatch. Declare a single blob or a single record parameter instead, or several of them.

Record parameters

Set type to the NSID of the record type you want, app.bsky.actor.profile for instance. The site owner picks one of their own records of that collection, or pastes an at:// URI and pins it, or types a record inline.

Whichever they choose, your tile receives the resolved record, not the reference. The reference itself is recorded separately, in /_bard/site.json's parameterSources, keyed by /{name}.

A record type accepts no constraint fields at all. Lexicon constrains a record with the schema it refers to, never with a maxLength.

Two things follow from the resolution being one level deep. Strong references inside a resolved record are not followed. And a blob inside a by-reference value is fetched from the referenced record's account, while a blob inside an inline value is fetched from the site owner's. Which is why copying somebody else's profile inline fails and pinning it by reference works.

Constraints

A constraint field is only legal on the type Lexicon defines it for. This table is the rule.

type Constraint fields it accepts
string format, maxLength, maxGraphemes, enum, knownValues
integer minimum, maximum
boolean none
blob accept, maxSize
array maxLength, and items, which is required
an NSID none

name, type, required and description are not constraints and are legal on everything, which is why they appear in no row.

Constraint Applies to Meaning
format string One of eleven Lexicon string formats: at-identifier, at-uri, cid, datetime, did, handle, language, nsid, record-key, tid, uri
maxLength string Length in UTF-8 bytes. Minimum 1
maxLength array Maximum item count. Minimum 1
maxGraphemes string Length in grapheme clusters. Minimum 1
minimum integer Inclusive lower bound
maximum integer Inclusive upper bound. Keep it below 2^53, because larger integers lose precision in the JavaScript that reads them
enum string A closed set of permitted values, at most 64 of them. Strings only
knownValues string An open, advisory set, at most 64 of them
accept blob Accepted MIME types, at most 16, wildcards as in Lexicon (image/*)
maxSize blob Maximum size in bytes. Minimum 1

enum beats everything else a string declares. A string with both enum and format: uri renders as a menu and not a URL box. There is nothing to type, so the format never comes up. knownValues is the reverse: it is tested after format, so format: uri plus knownValues renders a URL box and the suggestions are dropped.

A constraint on the wrong type is an error

It is not quietly ignored. maxLength on an integer makes your tile invalid, and an invalid tile cannot be bound by anybody. The message names the field and the type:

`params` entry "count": `maxLength` is not a constraint Lexicon defines for `integer`
      

The same holds for format. The set is closed against what Lexicon actually implements: the eleven the schema names, plus space-ref, which the builder does not offer and the form does not check. So format: "email" is refused on your tile, not on every site that tries to supply a value for it.

There are no defaults

You cannot declare a default value for a parameter, anywhere. That is deliberate, not missing.

Leave an optional parameter empty and it is left out entirely. Your description is the only place you get to say what your tile does when that happens, and your code must handle the absence:

const heading = params.heading ?? profile.displayName;
      const count = params.count ?? 10;
      

Descriptions are the help text

The site owner's form is generated from your params array, in declaration order, one control per parameter, with your description rendered as the help text under the label. You will not be in the room. Write it as though somebody is reading it, because they are.

A good description says what the value is for, where it appears, and what happens if it is left blank. A poor one restates the label.

Good:  Shown above the profile. Defaults to the display name.
      Poor:  The heading.
      

For a required parameter, say what it is for and what a reasonable value looks like. "The profile record that is rendered" is doing more work than "profile" is.

The form your declaration becomes

Each type maps to one control. A tile that declares nothing renders "This tile declares no parameters."

Declared Control
string with a non-empty enum A menu, with an empty first option reading "Choose one…" when required and "(not set)" when not
string, format: did / handle / at-identifier A text box that resolves the identity as it is typed and stores the canonical form
string, format: datetime A date-and-time picker
string, format: uri A URL box
string with knownValues A text box with a suggestion list
string, anything else A text box
integer A number box, with min and max from minimum and maximum
boolean A checkbox
blob A file picker, filtered by accept, uploading to the owner's own account
array Repeatable rows of the items control, with Add item disabled at maxLength
an NSID Two tabs, By reference and Inline (advanced), with By reference selected

The site editor's parameters form, generated from a tile's declaration, showing a text field with a character counter, a file upload and a record picker.

Declaring maxLength or maxGraphemes adds a character counter under the field. It shows one budget instead of two, whichever of the two is proportionally fuller, until the value goes over one of them. Then both appear.

A blob field says "No file chosen. At most {maxSize}." when you declared one, then "Uploading to your PDS…", then the type and size it stored. A record field says "Nothing pinned yet. Highport stores the URI and the CID it has right now." until they choose, then names the URI and the CID it pinned.

How a failure reaches the site owner

Two passes, and they see both.

As they type. Every constraint you declare becomes a check the form runs when they leave the field, carrying the error name the server would have used. MissingParameter for an empty required field, ParameterConstraintViolation for a broken bound, ParameterTypeMismatch for the wrong kind of value. So the sentence somebody reads while typing is the sentence they would have read after a round trip. Using a tile on your site lists what each one says. These are a courtesy, not the rule.

When they validate. space.highport.manage.validate is the authority, and it returns parameter faults in parameterErrors rather than errors. The site editor lists them under the heading "Fix these in your details above". They are not attached to individual fields, so a long form with one broken value takes some reading.

The message carries the validator's own wording, prefixed with the parameter name, which is why a site owner sees the limit expressed the way their PDS would express it:

parameters.count: must be ≤ 50
      

Every error a binding can produce

These are the names a site owner meets when their values do not match your declaration. Each is reported against parameters.{name} and, for a value nested inside a record or an array, the JSON Pointer to the exact spot.

Name What happened
MissingParameter A required parameter the record does not supply
UnknownParameter A supplied name the tile does not declare
ParametersWithoutTile parameters supplied beside an inline manifest rather than a tile binding
ParametersTooLarge The resolved JSON exceeds 65 536 bytes. Measured after every reference is resolved, so a large referenced record counts. Blobs do not
ParameterTypeMismatch The value is not of the declared kind: a string where an integer was declared, a record carrying the wrong $type, a strong reference supplied for a primitive
ParameterConstraintViolation The right kind, a broken constraint
ParameterPathUnrepresentable A blob sits at a pointer whose typed path cannot be formed
ParameterRecordNotFound A reference naming a record we will not read
ParameterRecordVersionUnavailable The record exists at that URI, at a different CID. The binding is by CID, so the owner rebinds to pick the change up
ParameterRecordCidMismatch The account served a record that does not hash to the pinned CID

A pass reports every fault it finds instead of stopping at the first. The first becomes the record's error name and the rest are appended as (and N more: …), so a record with four broken parameters is one publish cycle and not four.

Every error your declaration can produce

All of these are TileInvalid on your own tile record, and all are terminal: the tile is not bindable until you publish a version that fixes them. Everything from the third row down is prefixed with the entry label, so a real message reads `params` entry "count": `maxSize` is not a constraint Lexicon defines for `integer`.

Rule Message
At most 32 entries `params` declares N entries; the limit is 32
Names unique within a tile `params` declares "x" more than once; a site supplies one value per name and a repeated name has no single declaration to check it against
name is not empty `name` is empty
name does not begin with _ `name` begins with `_`, which is reserved: parameters are materialized under `/_bard/params/`
name matches [A-Za-z][A-Za-z0-9_]* `name` "a-b" does not match [A-Za-z][A-Za-z0-9_]*
type is a primitive or a valid NSID `type` "Foo" is neither a Lexicon primitive (string, integer, boolean, blob, array) nor a valid NSID: …
Constraints only where Lexicon defines them `maxSize` is not a constraint Lexicon defines for `string`
format is one Lexicon implements `format` "email" is not a Lexicon string format, so no supplied value could ever satisfy it
maxLength, maxGraphemes, maxSize are at least 1 `maxLength` is 0; the lexicon's minimum is 1
array declares items `array` declares no `items`, so there is no element type to check a supplied array against
items only on an array `items` belongs to `array` and this parameter is `string`
items.type is never array `items.type` is `array`; an array parameter is one level deep
The same rules one level down The same messages, prefixed items.

A parameter whose type is unusable has its constraint checks skipped, because guessing which row to check against would report fields as misplaced that may be exactly right once the type is fixed.

Two of these only reach you if you write the record by hand: a constraint on the wrong type, and an unknown format. The tile builder in the Hub drops every constraint that no longer applies when you change a parameter's type, and it offers the eleven formats as a menu instead of a text field, so neither is reachable through it.

Declaring well

Fewer parameters, more clearly named. Every one you add is another decision somebody has to make before their page exists, and the fifth field is where people close the tab.

Orbital Control has watched a great many people abandon a form at the fourth required field.

Related: Tiles for what a tile is and how binding pins a version, Building a tile for reading these values at runtime, Versions for what changing a declaration does to sites already bound, and Using a tile on your site for the other side of the form.

Tiles · 3 of 4