SemiLayerDocs

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."

/semilayer watch analyze --name=byCategory --every=daily --at=09:00 --lens=products
/semilayer remind me weekly to search refunds --lens=tickets
/semilayer remind me every monday at 10am to analyze cuisine --lens=recipes

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:

  1. Loads the watch row + workspace + creator link.
  2. Mints a platform JWT for the creator (60s TTL, scoped to their org membership).
  3. Calls the wrapped command's API endpoint.
  4. Renders the result via the appropriate chat formatter.
  5. Posts to the watch's target channel.
  6. 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

--everyWhat it does
hourlyAt most every 60 minutes from the last fire. No clock anchoring.
dailyOnce per day at --at=HH:MM (default 09:00) in --tz= (default UTC).
weeklyOnce 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:

PhraseMaps 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:

/semilayer remind me daily to status --env=production
/semilayer remind me weekly to analyze cuisine --lens=recipes --kind=donut

Targets

By default, watch posts to the channel you ran it from and remind posts to a DM. Override with --target=.

--targetWhere 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

/semilayer watch list                # see your watches in this channel
/semilayer watch list --channel      # admin: every watch in this channel
/semilayer watch list --all          # admin: every watch in the workspace

/semilayer watch run <id>            # fire right now (creator only)
/semilayer watch pause <id>          # stop firing, keep config
/semilayer watch resume <id>         # un-pause
/semilayer watch stop <id>           # soft-delete

# Channel admins (owner / admin role) can stop watches that post into
# their channel, even if they didn't create them. Useful when somebody
# leaves the team but their watches keep firing.

Caps & quotas

  • 25 watches per user per workspace. Hit the cap and watch create returns 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 apiRequestsPerMinute tier.

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).