Docs menu

Jobs and email

Two things an app can do when nobody has it open: run on a schedule, and email the people who use it. Both are declared in spryloom.yaml and shown on the app's page before anyone signs in.


Scheduled jobs

app:
  name: Office requests
  slug: office-requests
  description: Tracks office requests and reminds approvers every weekday.
runtime:
  frontend: none
  backend: node
  start: node server.js
access:
  visibility: invited
data:
  postgres: true
  tables: [requests]
  uploads: false
jobs:
  - name: morning-reminder
    command: node jobs/remind.js
    schedule: "0 9 * * 1-5"
    timezone: America/New_York

A job is a script in your app. It runs with the app's database and secrets, on the same network with the same limits, so it can read and write what the app can and reach nothing the app cannot. It is stopped when the command exits, or at timeout_minutes.

What the beta allows:

  • At most one run every 10 hours per job: once or twice a day. A schedule that runs more often is refused when you publish, with one that works.
  • At most 5 jobs per app, and 15 minutes per run.
  • If a run is still going when the next is due, the next one is skipped.

When a job fails. A run that exits with anything but 0 is recorded as failed, with the last lines it printed. After 5 failures in a row the job is paused and you are emailed. Publishing again, or spry jobs resume, starts it again.

spry jobs office-requests                              when each job runs, and how the last run went
spry jobs run office-requests morning-reminder         run it now
spry logs office-requests --job morning-reminder       its recent runs and what it printed
spry jobs resume office-requests morning-reminder      after it was paused

Email to the people who use the app

email: true

The app sends a notification to Spryloom, and Spryloom emails it. Spryloom tells the app where with SPRYLOOM_GATEWAY_URL and a key of its own with SPRYLOOM_GATEWAY_KEY. Use the helper your coding agent adds with spry skill, or send it yourself:

const answer = await fetch(`${process.env.SPRYLOOM_GATEWAY_URL}/v1/notify`, {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.SPRYLOOM_GATEWAY_KEY}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    to: 'jo@yourcompany.com',
    subject: '2 requests are waiting for your approval',
    text: 'Replace meeting-room monitor, and new chairs for room 4.',
    button: { label: 'Review requests', path: '/approvals' },
    dedupeKey: 'reminder-2026-09-22-jo',
  }),
});
Field
to One address: someone invited to the app, or who has signed in to it.
subject Up to 100 characters.
text Plain text, up to 500 characters, with no web addresses in it. Anything shaped like one is refused, including words such as "Node.js" or "report.pdf".
button Optional. A label of up to 30 characters, and a path in your app, such as /requests/42.
dedupeKey Optional. The same key within a day sends one email however many times you ask.

What the email looks like. It comes from "Your app via Spryloom", from the address your invitations come from, with your address as the reply-to. Spryloom lays it out the same way every time: your subject, your text, your button, and a footer saying you wrote it, why the person is getting it, and how to stop it.

Why no links. The email comes from the same address as sign-in links. If an app could put any link in it, a message could send someone to a page that looks like Spryloom's sign-in. The button can only open a page in your app, which is behind sign-in. Anything richer belongs on that page.

What the beta allows:

  • At most 5 notifications per app per day. The sixth is refused, and you are emailed once.
  • Only to people who can open the app. Anyone else is refused, and the refusal says so.
  • Anyone can stop an app's emails with the link in the footer. Their sign-in emails still arrive.
  • If people mark an app's notifications as spam twice in 30 days, its email is stopped and you are told.

The answer to each request says what happened. 202 means it was accepted. A refusal carries a message saying why, for example:

422 not_a_member: jo@example.com does not have access to Office requests. Invite them first, or send to someone who has.
429 daily_limit: Office requests has sent 5 notifications today, the limit during the beta. Sending starts again at midnight UTC.

Retry a 503 with the same dedupeKey: it is sent once.