Watch & Remind
Schedule any read or analyze command to fire on a recurring cadence. Two surfaces:
/semilayer watch <wrapped-cmd> --every=…— explicit, structured syntax. Required for analyze + non-default channels./semilayer remind me <cadence> to <command>— natural language for the common case. Defaults to DM target.
Both write to the same chat_watches table; both fire from the same
worker tick. The split is purely ergonomic — remind for "I want
this in my inbox," watch for "the team should see this in #data."
How firing works
Watches fire as the creator. A watch you set up runs with your RBAC at fire time. The platform mints a 60-second JWT scoped to your identity, makes the underlying API call, and posts the result. If your role drops below what the watch needs, the watch soft-pauses and you get a DM with a link to fix it.
Once a minute, a worker job (chat-watch.tick) scans every active,
non-paused watch. For each one it computes:
- Has the schedule's window opened in the watch's tz?
- Has it not already fired in that window?
If yes to both, it dispatches a chat-watch.fire job with a
singleton key on the watch id (so a tick + manual watch run
don't fire twice).
The fire job:
- Loads the watch row + workspace + creator link.
- Mints a platform JWT for the creator (60s TTL, scoped to their org membership).
- Calls the wrapped command's API endpoint.
- Renders the result via the appropriate chat formatter.
- Posts to the watch's target channel.
- Updates
last_fired_at+last_fire_status.
A footer on every fired post identifies the watch:
🔁 Watch <id> · daily at 09:00 · created by <email>.
Cadence syntax
--every | What it does |
|---|---|
hourly | At most every 60 minutes from the last fire. No clock anchoring. |
daily | Once per day at --at=HH:MM (default 09:00) in --tz= (default UTC). |
weekly | Once per week on `--dow=mon |
All cadences are at-most-once-per-window. A watch that fires at 09:00 doesn't fire again at 09:30 even if you somehow trigger the tick early.
remind natural language
The parser handles the high-frequency cases:
| Phrase | Maps to |
|---|---|
me hourly to <cmd> | --every=hourly |
me daily to <cmd> | --every=daily --at=09:00 |
me daily at 9am to <cmd> | --every=daily --at=09:00 |
me daily at 14:30 to <cmd> | --every=daily --at=14:30 |
me weekly to <cmd> | --every=weekly --dow=mon --at=09:00 |
me every monday at 10am to <cmd> | --every=weekly --dow=mon --at=10:00 |
me friday at 5pm to <cmd> | --every=weekly --dow=fri --at=17:00 |
me morning to <cmd> | --every=daily --at=09:00 |
After the to, anything is forwarded as a normal command:
Targets
By default, watch posts to the channel you ran it from and
remind posts to a DM. Override with --target=.
--target | Where it posts |
|---|---|
this | (default for watch) — the channel where you ran the command |
dm | (default for remind) — DM to you |
C12345… | A specific Slack channel id / Discord channel id |
The bot must be present in the target channel. If it isn't (or you
DM-targeted yourself but blocked the bot), the fire is recorded as
channel_unreachable and the watch keeps running — the next fire
re-tries the post.
Lifecycle
Caps & quotas
- 25 watches per user per workspace. Hit the cap and
watch createreturns a clear pointer to stop one first. - No org-wide cap today — the per-user cap × member count is the effective ceiling. We'll add a soft org cap of 250 when telemetry shows it's needed.
- Watches don't carry a separate quota. Each fire is one platform
API call against your existing
apiRequestsPerMinutetier.
Failure modes
Watch fires but result is empty. — The wrapped command returned no rows. The post still happens (so you know the watch is alive) but with a "no results" body. Stop / pause if that's not what you want.
Watch soft-paused with rbac_denied. — Your role on the bound
org dropped below what the wrapped command needs. The platform DMs
the creator with a link to either re-grant the role or
/semilayer watch resume <id> after you fix it.
Watch fires the wrong analyze / search. — The wrapped command
runs as configured at create time, not as the lens looks today.
If the analyze has been deleted from the lens config, the fire
records lens_missing and the watch is paused.
Daily watch fires twice on a DST boundary. — It shouldn't —
schedule math uses Intl.DateTimeFormat to compute the local-time
window, which DST-adjusts automatically. If you see a duplicate fire,
file a bug with the watch id; we'll grep the worker logs.
API equivalent
Watches don't have a public REST API today — they're a chat-only
surface. Programmatic scheduling happens via your own cron or the
POST /v1/<command> endpoints (which the watch fire handler calls
internally).