agentleFS
SELISEdigitalplatformsSELISEdigitalplatforms/blocks-skills/skills/blocks-notifier/SKILL.md

blocks-notifier

Skill42 starsChanged 12 days agoMITView on GitHub

Push real-time/offline notifications and manage a signed-in user's own notification inbox, via both the SDK (`blocksClient.notifier.*`) and the CLI (`blocks notifier notify|list|unread|mark-read|mark-all-read`). Distinct from the sibling blocks-notification skill, which configures tenant notification-*channel* settings on a different backing service, not sending. `notifier unread` flattens its subscription filter into GET query params since Fetch forbids a GET body. `--dry-run` before `--yes` on CLI `notify`/`mark-read`/`mark-all-read`.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    When invoking a project-scoped blocks command, either use the resolved account's saved selection or pass --project <tenantId> for that one …

    line 6
  2. Distinctive:

    Every CLI command is project-scoped: each requires an impersonated project session, and there is no account-level mode for any of the five.

    line 24

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

When invoking a project-scoped blocks command, either use the resolved account's saved selection or pass --project <tenantId> for that one command without changing saved state. --project applies to CLI commands only, never SDK calls.

Blocks Notifier — Send & Inbox

notifier pushes real-time/offline notifications to users, roles, or subscription-filter matches, and reads/manages the signed-in user's own notification inbox. This is a separate, deliberate concern from the blocks-notification skill, which manages a tenant's notification-channel configuration — which channel/method a notification type uses — on an unrelated backing service. Both this skill and the sibling skill confirm the same distinction from their own side. Don't merge them, and don't reconcile them as if one were a typo for the other — they hit different backing services. If the ask is "configure which channel a notification type uses," route to the blocks-notification skill instead.

Unlike blocks-notification (100% CLI, no SDK path), notifier has both a CLI and an SDK surface, and every one of the five operations exists on both:

OperationCLISDK (blocksClient.notifier.*)
Send a notificationblocks notifier notifynotify(request)
List the inboxblocks notifier listgetNotifications(options)
Unread by subscription filterblocks notifier unreadgetUnreadNotificationsBySubscriptionFilter(request)
Mark one readblocks notifier mark-read <id>markNotificationAsRead(request)
Mark all readblocks notifier mark-all-readmarkAllNotificationAsRead()

CLI — blocks notifier *

Every CLI command is project-scoped: each requires an impersonated project session, and there is no account-level mode for any of the five. Project resolution is --project <tenantId>, then the nearest workspace blocks.json (which blocks use <tenantId> writes), then — only outside any workspace — the resolved account's selected project. See the blocks-bootstrap skill if account or project context is unknown.

Once context is known, AI automation should pass it explicitly:

blocks notifier <command> --account <name> --project <tenantId> --json
  • blocks notifier notify [--user-ids a,b] [--roles a,b] [--organization-ids a,b] [--subscription-filters '<json>'] --denormalized-payload <text> [--configuration-name <n>] [--save-denormalized-payload-as-object] [--content-available] [--connection-id <id>] [--response-key <k>] [--response-value <v>] [--body '<json>'|--file <path>] [--dry-run] [--yes] [--json] — needs a target (--user-ids/--roles/--organization-ids/--subscription-filters) and --denormalized-payload; both are checked before anything is sent (notify_target_required, notify_payload_required). --user-ids and --roles are comma-separated lists; --subscription-filters is a raw JSON array string (e.g. [{"context":"orders","actionName":"created","value":"*"}], matching BlocksNotifierSubscriptionFilter[]). The body is built by merging --body/--file first, then layering the convenience flags on top — so an explicit convenience flag always wins over the same field in --body/--file, and an unset one never overwrites what --body/--file supplied. --content-available and --save-denormalized-payload-as-object are true-only booleans (absent when not passed, never an explicit false).

Every send goes through a notification configuration. --configuration-name is the name of one of the project's configurations — see blocks-notification (blocks notification list --json). When it is omitted and the project has exactly one, that one is used and the dry-run says so; with none or several, the command stops and says which. A name that does not exist is notification_configuration_not_found. The configuration's type decides who can receive: a user-specific configuration needs user ids, roles or organization ids; a filter-specific one needs --subscription-filters.

--connection-id, --response-key and --response-value are required by the service but carry nothing for a server-side send (the sender's SignalR connection, and two labels stored on the notification), so they default to server / notification / notification. A send needs a token whose role has blocks-logic::notifier::send (the services also accept an internal sender secret the CLI cannot produce); without it the service answers 401 and the CLI stops with permission_required. The CLI sends with its project session, and blocks-logic currently looks configurations up in the root project for such a session — so a project's own configuration fails with notify_impersonation_unsupported until that is fixed server-side. Send from the app with the SDK meanwhile.

  • blocks notifier list [--unread-only] [--page <n>] [--page-size <n>] [--sort-by <property>] [--sort-desc] [--filter <text>] [--json] — read-only.
  • blocks notifier unread [--user-id <id>] [--context <c>] [--action-name <a>] [--value <v>] [--order-by <1|2>] [--json] — read-only. See "The GET-with-a-body quirk" below.
  • blocks notifier mark-read <id> [--dry-run] [--yes] [--json] — id may be positional or --id; one of the two is required (Missing --id if neither given).
  • blocks notifier mark-all-read [--dry-run] [--yes] [--json] — no body/arguments needed.

The GET-with-a-body quirk (unread)

This operation is documented upstream as a read with a JSON request body, which the Fetch spec forbids sending on a plain GET-style read. Both the CLI and SDK work around it the same way: flatten the filter fields into the query string instead. Confirmed field names, read directly from source:

UserId
SubscriptionFilterData.Context
SubscriptionFilterData.ActionName
SubscriptionFilterData.Value
OrderBy

CLI flags map to them as --user-id -> UserId, --context -> SubscriptionFilterData.Context, --action-name -> SubscriptionFilterData.ActionName, --value -> SubscriptionFilterData.Value, --order-by -> OrderBy (1 = CreatedTime, newest first; 2 = ReadStatus — unread grouped ahead of read).

This flattening is a best-effort workaround, and the backend does not currently honor it: the service declares this read as taking its filter in the request body, so query parameters do not reach the filter at all — expect a missing-body rejection (or an unfiltered result) until the service is changed to read the filter from the query string. Prefer blocks notifier list --unread-only for "what's unread"; keep unread for the day the server side is fixed, and if a live call errors, that is why.

SDK — blocksClient.notifier.*

import { blocksClient } from "../../lib/blocks/client";

await blocksClient.notifier.notify({
  configurationName: "in-app",   // the `name` of a notification configuration in this project
  userIds: ["user-1"],
  roles: ["admin"],
  denormalizedPayload: '{"orderId":"123"}',
  saveDenormalizedPayloadAsAnObject: true
});

const inbox = await blocksClient.notifier.getNotifications({ isUnreadOnly: true, page: 1, pageSize: 20 });

const unread = await blocksClient.notifier.getUnreadNotificationsBySubscriptionFilter({
  orderBy: 1,
  userId: "user-1",
  subscriptionFilterData: { context: "orders", actionName: "created" }
});

await blocksClient.notifier.markNotificationAsRead({ id: notificationId });
await blocksClient.notifier.markAllNotificationAsRead();
  • notify(request: BlocksNotifyRequest) — fields: configurationName: string (required — a configuration name from this project), denormalizedPayload: string (required), userIds?: string[], roles?: string[], organizationIds?: string[], subscriptionFilters?: BlocksNotifierSubscriptionFilter[] (at least one target), saveDenormalizedPayloadAsAnObject?: boolean, contentAvailable?: boolean, and connectionId? / responseKey? / responseValue?, which the SDK fills with server / notification / notification when omitted. A missing or unknown configuration answers 400 with errors.ConfigurationName. The signed-in user's role needs blocks-logic::notifier::send, or the service answers 401.
  • getUnreadNotificationsBySubscriptionFilter(request) — fields: orderBy: 1 | 2 (1 CreatedTime desc, 2 ReadStatus — required in practice, see below), subscriptionFilterData?: { actionName?, context?, value? }, userId?. Internally builds the same flattened query as the CLI (see above) — this is the SDK-side half of the same documented workaround.
  • getNotifications(options: BlocksGetNotificationsOptions = {}) — options: filter?, isUnreadOnly?, page?, pageSize?, sortBy?, sortDescending?. Response shape: { notifications: Record<string, unknown>[], totalNotificationsCount: number, unReadNotificationsCount: number }.
  • markAllNotificationAsRead() — no arguments.
  • markNotificationAsRead(request: BlocksMarkNotificationAsReadRequest) — request: { id: string }.

BlocksOfflineNotification (the shape of items getUnreadNotificationsBySubscriptionFilter resolves to) is a loose Record<string, unknown> plus known fields correlationId?, createdTime?, denormalizedPayload?, id?, isRead?, payload?, readByRoles?: string[], readByUserIds?: string[]. BlocksNotifierPassThroughResponse (the notify/mark-read/mark-all-read return type) is an untyped Record<string, unknown> — the SDK doesn't shape it further; don't assume fields beyond what a live response actually contains.

The SDK methods don't take a project/tenant argument per call — project context comes from however the app's shared blocksClient instance was constructed (its xBlocksKey/appDomain), same as every other blocksClient.* namespace. Don't create a second client just for notifier calls.

Mutation discipline (CLI only)

notify, mark-read, and mark-all-read are the three CLI mutations, and all three follow the same two-gate pattern used throughout this CLI:

  1. --dry-run short-circuits before any network call or confirmation prompt, printing a preview of what would be sent (mark-all-read's preview has no request body since it sends none).
  2. Without --dry-run, a confirmation step accepts --yes outright or, interactively, prompts to type "yes" to continue, and cancels on anything else.

Always show the --dry-run output and get explicit approval before re-running with --yes. list and unread are read-only and have neither flag — don't tell a user to --dry-run a list or unread call. The SDK methods have no equivalent gate at all; that discipline is a CLI-only convention for terminal/CI operators, not something app code needs to replicate.

Gotchas

  • notifier and notification are not the same thing, and this is not an oversight to fix. notifier (this skill) sends notifications and reads a user's inbox, on both CLI and SDK. notification (the sibling skill) configures a tenant's notification channel settings, CLI-only. Don't answer a "send a notification" ask with notification save, and don't answer a "configure the channel" ask with notifier.
  • notifier unread's query-param flattening is an inferred client-side workaround for a Fetch-spec conflict, not verified against a live call. See "The GET-with-a-body quirk" above. If it ever errors in practice, re-check whether the real endpoint tolerates a body server-side (some non-browser/non-Node HTTP stacks do) before assuming the flattening itself is broken.
  • --content-available and --save-denormalized-payload-as-object on notify are true-only flags. Passing them sends true; omitting them omits the field entirely — there's no way to send an explicit false through the convenience flags (use --body/--file for that).
  • Convenience flags on notify win over --body/--file. The merge order is --body/--file first, then the individual flags spread on top — so a flag like --connection-id always overrides the same key in --body if both are given.
  • order-by is 1 = CreatedTime (newest first) or 2 = ReadStatus. There is no 0, and that is the trap: the service switches on the value and returns an empty list for anything else — including the 0 an omitted field serializes to. A caller that forgets orderBy gets zero notifications back and reads it as "nothing unread," not as a bad request. Always send 1 or 2 explicitly.
  • Every CLI command is project-scoped; there's no account-level mode. The SDK has no per-call project argument — it inherits whatever project the shared blocksClient was configured for.
  • mark-read's id (positional or --id) is always required — never guessed or defaulted.
  • --dry-run before --yes, always, on the three CLI mutations — same discipline as every other mutating blocks command in this pack.

Example trigger prompts

  • "Send a notification to these user IDs from my app." -> blocks notification list --json for the configuration name, then SDK blocksClient.notifier.notify({ configurationName, userIds: [...], denormalizedPayload }).
  • "Push a notification to everyone matching this subscription filter." -> SDK notify({ subscriptionFilters: [...] }), or blocks notifier notify --subscription-filters '<json>' --dry-run --json from the terminal.
  • "Show me a user's unread notifications for the 'orders' context." -> blocks notifier unread --user-id <id> --context orders --order-by 1 --json, or SDK getUnreadNotificationsBySubscriptionFilter({ orderBy: 1, userId, subscriptionFilterData: { context: "orders" } }).
  • "List my notification inbox, unread only." -> blocks notifier list --unread-only --json, or SDK getNotifications({ isUnreadOnly: true }).
  • "Mark this notification as read." -> blocks notifier mark-read <id> --dry-run --json, then --yes.
  • "Mark everything in the inbox as read." -> blocks notifier mark-all-read --dry-run --json, then --yes.
  • "Configure which channel the order-shipped notification uses." -> not this skill; that's the blocks-notification skill.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.