Skip to content

Script types and triggers

A script is identified by its trigger string. The trigger is the script’s id: there is no separate name. It has the form <prefix>:<suffix>, and the prefix decides which events the script receives.

TriggerFires whenSuffix
xrpc.query:<nsid>The XRPC query <nsid> is calledAn NSID
xrpc.procedure:<nsid>The XRPC procedure <nsid> is calledAn NSID
record.index:<nsid>Any record in collection <nsid> is created, updated or deletedAn NSID
record.create:<nsid>A record in <nsid> is createdAn NSID
record.update:<nsid>A record in <nsid> is updatedAn NSID
record.delete:<nsid>A record in <nsid> is deletedAn NSID
labeler.apply:<nsid>A label arrives whose subject is at://<did>/<nsid>/<rkey>An NSID
labeler.apply:_actorA label arrives whose subject is a bare DIDThe literal _actor
job.run:<type>A background job of type <type> is picked up by the workerA job type

Any other prefix is refused. The suffix can’t be empty. A job type may not end in .test, because in a project src/jobs/<type>.test.<ext> is a test file.

In a project, you don’t write the trigger. A script’s path under src/ decides it, and the file’s extension decides its language:

FileTrigger
src/lexicons/<path>/<name>.<ext>, next to a query lexicon <name>.jsonxrpc.query:<nsid>
src/lexicons/<path>/<name>.<ext>, next to a procedure lexicon <name>.jsonxrpc.procedure:<nsid>
src/lexicons/<path>/<name>.index.<ext>, next to a record lexicon <name>.jsonrecord.index:<nsid>
src/lexicons/<path>/<name>.create.<ext>, .update.<ext> or .delete.<ext>record.create:<nsid>, record.update:<nsid> or record.delete:<nsid>
src/labels/<nsid>.<ext>labeler.apply:<nsid>
src/labels/_actor.<ext>labeler.apply:_actor
src/jobs/<type>.<ext>job.run:<type>

For example, src/lexicons/xyz/statusphere/status.create.lua next to status.json is the script for record.create:xyz.statusphere.status.

A file that names a trigger it can’t have fails the build, with a message saying why:

  • A record script needs a record lexicon next to it. status.create.lua with no status.json, or next to a query lexicon, is an error.
  • A record lexicon has four triggers, so a bare status.lua next to a record lexicon is an error that asks you to name the action.
  • A definitions or space lexicon runs no script, so a file named after one is an error.
  • src/jobs/ and src/labels/ are flat: a file in a subdirectory is an error. A name the trigger grammar refuses, such as src/jobs/Nightly.lua, is an error too.
  • A script in an extension no installed interpreter claims is an error that lists the extensions that are claimed.

*.test.<ext> is a test file wherever it sits. Any other file with a claimed extension is a module that scripts can import. A .md, .markdown or .txt file where a script would go is ignored, so a README can sit next to the code it describes.

For a record event, HappyView first looks for a script on the action’s own trigger, such as record.create:<nsid>. If there is none, it uses record.index:<nsid>. So record.index works as a catch-all, and you can override single actions next to it.

No other trigger falls back. XRPC, label and job triggers must match exactly.

A query or procedure script handles the request and its return value is the response body. Without a script, HappyView uses its default query and procedure behavior.

An XRPC script fails closed and runs once. An error becomes an error response to the caller. See errors.

A record script runs before the record is indexed, so it can skip the record, rewrite it, or trigger side effects. It reacts to events from the network, including the echo of writes HappyView itself made to a PDS. It does not run on an XRPC request.

handle returnsEffect
Nothing (nil, undefined, null)The record is not indexed. On a delete, the delete is skipped and the record stays
An objectThat object is stored in place of the record
Anything else, such as trueThe record is stored as it arrived
(no script)The record is stored as it arrived

A delete event carries no record, so returning input.record on a delete returns nothing and skips the delete. Return true to let a delete through.

There is no caller session in a record script, so a library call that writes to a PDS as the caller fails. Reads, HTTP, and local index writes work. Record scripts can also run more than once for the same record; see Create scripts may run more than once.

A label script runs when a label arrives from a subscribed labeler, before the label is stored.

handle returnsEffect
NothingThe label is not stored
An objectIts fields are merged over the label. Fields it leaves out keep their original values
Anything else, such as trueThe label is stored as it arrived
(no script)The label is stored as it arrived

ctx.caller_did is absent in a label script, because no repo is acting.

A job script runs when the background worker picks up a job of its type. Any script can create a job with create(job_type, input[, opts]) from the happyview.jobs library. Passing { auth: true } ({ auth = true } in Lua) carries the caller’s PDS session into the job.

The return value becomes the job’s result. A job is not held to the instruction limit or the wall clock; it should check ctx.job.should_stop() and return when asked. See Limits.

Script typeOn failure
Query, procedureOne attempt. The caller gets the error
Record, labelUp to 4 attempts in total, with 1, 2 and 4 second delays between them. After the last one, the event is written to the dead-letter table and the record or label is stored as it arrived
JobThe job’s status becomes failed, with the error message as its error

Record and label scripts fail open so a broken script can’t stop indexing. They run before indexing, so a slow or failing script holds up the events behind it: a script that fails every time spends about 7 seconds per event on backoff.

Create the file at the path for its trigger, as in Name a script file. hpy happyview dev picks it up when you save it, and hpy happyview deploy sends it to an instance.

Open Integrations > Scripts and click New script. Pick the trigger, the target, and, if more than one interpreter is installed, the language. Then click Create script.

With the admin API, POST /admin/scripts with the trigger as id:

Terminal window
curl -X POST "http://127.0.0.1:3000/admin/scripts" \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"id": "record.create:xyz.statusphere.status",
"script_type": "javascript",
"body": "export default (input) => input.record ?? true;",
"description": "Pass statuses through"
}'

script_type defaults to lua when left out. The body is checked by the interpreter for that language when you save, and a language no installed interpreter claims is refused with a 400. A description can be up to 300 characters.