@clawhub-notilens-97a2249e6c
Send real-time alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, and metric tracking.
---
name: notilens
description: Send real-time alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, and metric tracking.
version: 0.2.0
metadata:
openclaw:
requires:
env:
- NOTILENS_TOKEN
- NOTILENS_SECRET
primaryEnv: NOTILENS_TOKEN
emoji: "🔔"
homepage: https://www.notilens.com
---
# NotiLens Plugin for OpenClaw
This is a **code plugin** — all functions are callable directly by the agent at runtime. No curl needed.
Get your `NOTILENS_TOKEN` and `NOTILENS_SECRET` from your topic settings at https://www.notilens.com.
## Available Functions
### `notify(name, event, message, options?)`
Send a notification. Title is auto-generated from `name + event`. Options: `type`, `image_url`, `open_url`, `download_url`, `tags`, `meta`.
### `track(name, event, message, type?, meta?)`
Track any custom event (e.g. `order.placed`, `deploy.started`). Title is auto-generated.
### `taskStarted(name, taskId, message?, meta?)`
Fire `task.started` when execution begins.
### `taskProgress(name, taskId, message, meta?)`
Fire `task.progress` at meaningful checkpoints.
### `taskCompleted(name, taskId, message, meta?)`
Fire `task.completed` when a task finishes successfully. Include `total_duration_ms`, `active_ms`, and custom metrics in `meta`.
### `taskFailed(name, taskId, message, meta?)`
Fire `task.failed` when a task fails. Automatically sets `is_actionable: true`.
### `taskError(name, taskId, message, meta?)`
Fire `task.error` for non-fatal errors (task continues).
### `taskRetry(name, taskId, retryCount, meta?)`
Fire `task.retry` when retrying. Pass the current retry number (1-based).
### `taskLoop(name, taskId, message, loopCount, meta?)`
Fire `task.loop` when the same step is repeating. Pass the current loop count.
### `inputRequired(name, message, openUrl?, meta?)`
Fire `input.required` when a human decision is needed. Automatically sets `is_actionable: true`.
## Recommended `meta` Fields
| Key | Description |
|--------------------|-------------|
| `run_id` | Unique run ID — format `run_{unix_ms}_{hex4}` |
| `total_duration_ms`| Wall-clock time from task start to now |
| `active_ms` | Active time (excludes pauses/waits) |
| `retry_count` | Number of retries so far |
| `error_count` | Number of non-fatal errors |
| `loop_count` | Number of loop iterations |
| `last_error` | Last error message string |
## Configuration
```
NOTILENS_TOKEN=your_topic_token
NOTILENS_SECRET=your_topic_secret
```
Both are found in your topic settings at https://www.notilens.com.
FILE:claw.json
{
"name": "notilens",
"version": "0.2.0",
"description": "Send alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, and metric tracking.",
"author": "notilens",
"license": "MIT",
"entry": "src/notilens.js",
"skills": [
{
"id": "genRunId",
"description": "Generate a unique run ID (run_{unix_ms}_{hex4}) to correlate all events from the same task execution. Call once at task start and include in meta.run_id on every event.",
"module": "src/notilens.js",
"export": "genRunId"
},
{
"id": "notify",
"description": "Send a notification to NotiLens. Pass name (source), event, message, and optional options (type, image_url, open_url, download_url, tags, meta). Title is auto-generated.",
"module": "src/notilens.js",
"export": "notify"
},
{
"id": "track",
"description": "Track any custom event (e.g. order.placed, deploy.started). Title is auto-generated. Type and meta are optional.",
"module": "src/notilens.js",
"export": "track"
},
{
"id": "task.queued",
"description": "Fire task.queued when a task is placed in a queue before a worker picks it up.",
"module": "src/notilens.js",
"export": "taskQueued"
},
{
"id": "task.started",
"description": "Fire task.started when execution begins. Include queue_ms in meta if the task was queued first.",
"module": "src/notilens.js",
"export": "taskStarted"
},
{
"id": "task.progress",
"description": "Fire task.progress at meaningful checkpoints during a long task. Include rows_done, percent, tokens_used, or other metrics in meta.",
"module": "src/notilens.js",
"export": "taskProgress"
},
{
"id": "task.paused",
"description": "Fire task.paused when the task is pausing (e.g. rate limit, waiting on I/O). Include pause_count and wait_reason in meta.",
"module": "src/notilens.js",
"export": "taskPaused"
},
{
"id": "task.waiting",
"description": "Fire task.waiting when the task is blocked on an external resource. Include wait_count and wait_reason in meta.",
"module": "src/notilens.js",
"export": "taskWaiting"
},
{
"id": "task.resumed",
"description": "Fire task.resumed after a pause or wait ends. Include pause_ms or wait_ms in meta.",
"module": "src/notilens.js",
"export": "taskResumed"
},
{
"id": "task.retry",
"description": "Fire task.retry when the task is being retried. Pass the current retry number (1-based) as retryCount.",
"module": "src/notilens.js",
"export": "taskRetry"
},
{
"id": "task.loop",
"description": "Fire task.loop when repeating the same step. Pass the current loop count. Backend ML handles detection.",
"module": "src/notilens.js",
"export": "taskLoop"
},
{
"id": "task.error",
"description": "Fire task.error for a non-fatal error — task continues after this. Include error_count and last_error in meta.",
"module": "src/notilens.js",
"export": "taskError"
},
{
"id": "task.completed",
"description": "Fire task.completed when a task finishes successfully. Include total_duration_ms, active_ms, and custom metrics in meta.",
"module": "src/notilens.js",
"export": "taskCompleted"
},
{
"id": "task.failed",
"description": "Fire task.failed when a task fails and will not be retried. Include retry_count, error_count, last_error, and total_duration_ms in meta.",
"module": "src/notilens.js",
"export": "taskFailed"
},
{
"id": "task.timeout",
"description": "Fire task.timeout when a task exceeds its time limit. Include total_duration_ms and time_limit_ms in meta.",
"module": "src/notilens.js",
"export": "taskTimeout"
},
{
"id": "task.cancelled",
"description": "Fire task.cancelled when a task is cancelled before completion.",
"module": "src/notilens.js",
"export": "taskCancelled"
},
{
"id": "task.stopped",
"description": "Fire task.stopped when a task is stopped intentionally (not an error).",
"module": "src/notilens.js",
"export": "taskStopped"
},
{
"id": "task.terminated",
"description": "Fire task.terminated when a task is forcibly terminated.",
"module": "src/notilens.js",
"export": "taskTerminated"
},
{
"id": "input.required",
"description": "Fire input.required when a human decision is needed to continue. Pass openUrl to link to an approval UI.",
"module": "src/notilens.js",
"export": "inputRequired"
},
{
"id": "input.approved",
"description": "Fire input.approved when a human approves the request.",
"module": "src/notilens.js",
"export": "inputApproved"
},
{
"id": "input.rejected",
"description": "Fire input.rejected when a human rejects the request.",
"module": "src/notilens.js",
"export": "inputRejected"
},
{
"id": "output.generated",
"description": "Fire output.generated when output is produced (file, report, result). Pass download_url, open_url, or image_url in meta.",
"module": "src/notilens.js",
"export": "outputGenerated"
},
{
"id": "output.failed",
"description": "Fire output.failed when expected output could not be produced.",
"module": "src/notilens.js",
"export": "outputFailed"
}
],
"permissions": {
"network": true,
"env": [
"NOTILENS_TOKEN",
"NOTILENS_SECRET"
]
},
"engines": {
"node": ">=18"
},
"tags": ["notifications", "monitoring", "alerts", "observability"]
}
FILE:openclaw.plugin.json
{
"id": "notilens",
"name": "notilens",
"description": "Send real-time alerts to NotiLens from any script, app, or AI agent.",
"entry": "src/notilens.js",
"exports": {
"genRunId": "src/notilens.js",
"notify": "src/notilens.js",
"track": "src/notilens.js",
"taskQueued": "src/notilens.js",
"taskStarted": "src/notilens.js",
"taskProgress": "src/notilens.js",
"taskPaused": "src/notilens.js",
"taskWaiting": "src/notilens.js",
"taskResumed": "src/notilens.js",
"taskRetry": "src/notilens.js",
"taskLoop": "src/notilens.js",
"taskError": "src/notilens.js",
"taskCompleted": "src/notilens.js",
"taskFailed": "src/notilens.js",
"taskTimeout": "src/notilens.js",
"taskCancelled": "src/notilens.js",
"taskStopped": "src/notilens.js",
"taskTerminated": "src/notilens.js",
"inputRequired": "src/notilens.js",
"inputApproved": "src/notilens.js",
"inputRejected": "src/notilens.js",
"outputGenerated": "src/notilens.js",
"outputFailed": "src/notilens.js"
},
"configSchema": {
"type": "object",
"properties": {
"token": {
"type": "string",
"description": "NotiLens topic token. Found in your topic settings at notilens.com."
},
"secret": {
"type": "string",
"description": "NotiLens topic secret. Found in your topic settings at notilens.com."
}
},
"required": ["token", "secret"]
}
}
FILE:package.json
{
"name": "notilens",
"version": "0.2.0",
"description": "NotiLens plugin for OpenClaw — send alerts from any script, app, or AI agent",
"main": "src/notilens.js",
"license": "MIT",
"engines": {
"node": ">=18"
},
"openclaw": {
"extensions": ["executes-code"],
"compat": {
"pluginApi": "1.0"
},
"build": {
"openclawVersion": "1.0.0"
}
}
}
FILE:src/notilens.js
'use strict';
const WEBHOOK_URL = 'https://hook.notilens.com/webhook/{token}/send';
const USER_AGENT = 'notilens-clawhub/0.2.0';
// ── Internals ─────────────────────────────────────────────────────────────────
function getCredentials() {
const token = process.env.NOTILENS_TOKEN;
const secret = process.env.NOTILENS_SECRET;
if (!token || !secret) {
throw new Error(
'NOTILENS_TOKEN and NOTILENS_SECRET environment variables are required. ' +
'Get them from your topic settings at https://www.notilens.com.'
);
}
return { token, secret };
}
async function _deliver(payload) {
const { token, secret } = getCredentials();
const url = WEBHOOK_URL.replace('{token}', token);
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-NOTILENS-KEY': secret,
'User-Agent': USER_AGENT,
},
body: JSON.stringify({ ts: Date.now() / 1000, ...payload }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(
`NotiLens delivery failed: HTTP res.status — data.message || data.error || 'unknown error'`
);
}
return data;
}
function _meta(obj) {
return Object.keys(obj).length ? { meta: obj } : {};
}
// ── Helper ────────────────────────────────────────────────────────────────────
/**
* Generate a unique run ID to correlate all events from the same task execution.
* Format: run_{unix_ms}_{random_hex4}
* Include this in meta.run_id on every event for a given run.
*/
function genRunId() {
const hex = Math.floor(Math.random() * 0xffff).toString(16).padStart(4, '0');
return `run_Date.now()_hex`;
}
// ── Notify ────────────────────────────────────────────────────────────────────
/**
* Send a notification. Title is auto-generated from name + event.
*
* @param {string} name - Source name (app, script, agent, etc.)
* @param {string} event - Event name, e.g. "order.placed" or "disk.space.full"
* @param {string} message - Notification body text
* @param {object} [options] - type, image_url, open_url, download_url, tags, meta
*/
async function notify(name, event, message, options = {}) {
if (!event) throw new Error('event is required');
if (!message) throw new Error('message is required');
const { type = 'info', ...rest } = options;
return _deliver({
event,
title: `name | event`,
message,
type,
agent: name, // kept as "agent" for backend compatibility
...rest,
});
}
/**
* Track any custom event. Title is auto-generated from name + event.
* Use this for domain-specific events like "order.placed", "deploy.started", etc.
*
* @param {string} name
* @param {string} event - Any event string, e.g. "order.placed"
* @param {string} message - Notification body
* @param {string} [type] - "info" | "success" | "warning" | "urgent" (default: "info")
* @param {object} [meta] - Optional key-value pairs
*/
async function track(name, event, message, type = 'info', meta = {}) {
if (!event) throw new Error('event is required');
if (!message) throw new Error('message is required');
return _deliver({
event,
title: `name | event`,
message,
type,
agent: name, // kept as "agent" for backend compatibility
..._meta(meta),
});
}
// ── Task lifecycle ─────────────────────────────────────────────────────────────
/**
* Fire task.queued — task is queued before a worker picks it up.
*/
async function taskQueued(name, taskId, message = '', meta = {}) {
return _deliver({
event: 'task.queued',
title: `name | taskId | task.queued`,
message: message || `name | taskId queued`,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.started — begins executing a task.
* @param {object} [meta] - run_id, queue_ms, etc.
*/
async function taskStarted(name, taskId, message = '', meta = {}) {
return _deliver({
event: 'task.started',
title: `name | taskId | task.started`,
message: message || `name | taskId started`,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.progress — meaningful checkpoint during a long task.
* @param {object} [meta] - rows_done, percent, tokens_used, etc.
*/
async function taskProgress(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.progress');
return _deliver({
event: 'task.progress',
title: `name | taskId | task.progress`,
message,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.paused — task is pausing (rate limit, waiting on I/O, etc.).
* @param {object} [meta] - pause_count, wait_reason, etc.
*/
async function taskPaused(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.paused');
return _deliver({
event: 'task.paused',
title: `name | taskId | task.paused`,
message,
type: 'warning',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.waiting — task is blocked on an external resource.
* @param {object} [meta] - wait_count, wait_reason, etc.
*/
async function taskWaiting(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.waiting');
return _deliver({
event: 'task.waiting',
title: `name | taskId | task.waiting`,
message,
type: 'warning',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.resumed — task resumed after a pause or wait.
* @param {object} [meta] - pause_ms, wait_ms, pause_count, wait_count
*/
async function taskResumed(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.resumed');
return _deliver({
event: 'task.resumed',
title: `name | taskId | task.resumed`,
message,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.retry — task is being retried after a failure.
* @param {number} retryCount - Current retry number (1-based)
* @param {object} [meta] - last_error, etc.
*/
async function taskRetry(name, taskId, retryCount, meta = {}) {
return _deliver({
event: 'task.retry',
title: `name | taskId | task.retry`,
message: `name | taskId retrying (attempt retryCount)`,
type: 'warning',
agent: name,
task_id: taskId,
meta: { retry_count: retryCount, ...meta },
});
}
/**
* Fire task.loop — agent detected it is repeating the same step.
* @param {number} loopCount - How many times the step has repeated
* @param {object} [meta]
*/
async function taskLoop(name, taskId, message, loopCount, meta = {}) {
if (!message) throw new Error('message is required for task.loop');
return _deliver({
event: 'task.loop',
title: `name | taskId | task.loop`,
message,
type: 'warning',
agent: name,
task_id: taskId,
is_actionable: true,
meta: { loop_count: loopCount, ...meta },
});
}
/**
* Fire task.error — non-fatal error (task continues after this).
* @param {object} [meta] - error_count, last_error, etc.
*/
async function taskError(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.error');
return _deliver({
event: 'task.error',
title: `name | taskId | task.error`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
..._meta(meta),
});
}
// ── Terminal states ────────────────────────────────────────────────────────────
/**
* Fire task.completed — task finished successfully.
* @param {object} [meta] - total_duration_ms, active_ms, rows_processed, etc.
* @param {string} [meta.download_url] - Promoted to top-level field
* @param {string} [meta.open_url] - Promoted to top-level field
*/
async function taskCompleted(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.completed');
const { download_url, open_url, ...restMeta } = meta;
return _deliver({
event: 'task.completed',
title: `name | taskId | task.completed`,
message,
type: 'success',
agent: name,
task_id: taskId,
...(download_url ? { download_url } : {}),
...(open_url ? { open_url } : {}),
..._meta(restMeta),
});
}
/**
* Fire task.failed — task failed and will not be retried.
* @param {object} [meta] - retry_count, error_count, last_error, total_duration_ms
* @param {string} [meta.open_url] - Promoted to top-level field
*/
async function taskFailed(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.failed');
const { open_url, ...restMeta } = meta;
return _deliver({
event: 'task.failed',
title: `name | taskId | task.failed`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
...(open_url ? { open_url } : {}),
..._meta(restMeta),
});
}
/**
* Fire task.timeout — task exceeded its time limit.
* @param {object} [meta] - total_duration_ms, time_limit_ms, etc.
*/
async function taskTimeout(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.timeout');
return _deliver({
event: 'task.timeout',
title: `name | taskId | task.timeout`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
..._meta(meta),
});
}
/**
* Fire task.cancelled — task was cancelled before completion.
* @param {object} [meta] - total_duration_ms, etc.
*/
async function taskCancelled(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.cancelled');
return _deliver({
event: 'task.cancelled',
title: `name | taskId | task.cancelled`,
message,
type: 'warning',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.stopped — task was stopped intentionally (not an error).
* @param {object} [meta] - total_duration_ms, etc.
*/
async function taskStopped(name, taskId, message = '', meta = {}) {
return _deliver({
event: 'task.stopped',
title: `name | taskId | task.stopped`,
message: message || `name | taskId stopped`,
type: 'info',
agent: name,
task_id: taskId,
..._meta(meta),
});
}
/**
* Fire task.terminated — task was forcibly terminated.
* @param {object} [meta] - total_duration_ms, reason, etc.
*/
async function taskTerminated(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for task.terminated');
return _deliver({
event: 'task.terminated',
title: `name | taskId | task.terminated`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
..._meta(meta),
});
}
// ── Input ──────────────────────────────────────────────────────────────────────
/**
* Fire input.required — needs a human decision to continue.
* @param {string} [openUrl] - URL to open for the approval UI
* @param {object} [meta]
*/
async function inputRequired(name, message, openUrl = '', meta = {}) {
if (!message) throw new Error('message is required for input.required');
return _deliver({
event: 'input.required',
title: `name | input required`,
message,
type: 'warning',
agent: name,
is_actionable: true,
...(openUrl ? { open_url: openUrl } : {}),
..._meta(meta),
});
}
/**
* Fire input.approved — human approved the request.
*/
async function inputApproved(name, message, meta = {}) {
if (!message) throw new Error('message is required for input.approved');
return _deliver({
event: 'input.approved',
title: `name | input approved`,
message,
type: 'success',
agent: name,
..._meta(meta),
});
}
/**
* Fire input.rejected — human rejected the request.
*/
async function inputRejected(name, message, meta = {}) {
if (!message) throw new Error('message is required for input.rejected');
return _deliver({
event: 'input.rejected',
title: `name | input rejected`,
message,
type: 'warning',
agent: name,
is_actionable: true,
..._meta(meta),
});
}
// ── Output ─────────────────────────────────────────────────────────────────────
/**
* Fire output.generated — produced output (file, report, result, etc.).
* @param {string} [meta.download_url] - Promoted to top-level field
* @param {string} [meta.open_url] - Promoted to top-level field
* @param {string} [meta.image_url] - Promoted to top-level field
*/
async function outputGenerated(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for output.generated');
const { download_url, open_url, image_url, ...restMeta } = meta;
return _deliver({
event: 'output.generated',
title: `name | taskId | output.generated`,
message,
type: 'success',
agent: name,
task_id: taskId,
...(download_url ? { download_url } : {}),
...(open_url ? { open_url } : {}),
...(image_url ? { image_url } : {}),
..._meta(restMeta),
});
}
/**
* Fire output.failed — failed to produce expected output.
* @param {object} [meta] - last_error, etc.
*/
async function outputFailed(name, taskId, message, meta = {}) {
if (!message) throw new Error('message is required for output.failed');
return _deliver({
event: 'output.failed',
title: `name | taskId | output.failed`,
message,
type: 'urgent',
agent: name,
task_id: taskId,
is_actionable: true,
..._meta(meta),
});
}
// ── Exports ────────────────────────────────────────────────────────────────────
module.exports = {
genRunId,
notify,
track,
taskQueued,
taskStarted,
taskProgress,
taskPaused,
taskWaiting,
taskResumed,
taskRetry,
taskLoop,
taskError,
taskCompleted,
taskFailed,
taskTimeout,
taskCancelled,
taskStopped,
taskTerminated,
inputRequired,
inputApproved,
inputRejected,
outputGenerated,
outputFailed,
};
Send real-time alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, metric tracking, and custom alerts.
---
name: notilens
description: Send real-time alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, metric tracking, and custom alerts.
version: 0.2.0
metadata:
openclaw:
requires:
env:
- NOTILENS_TOKEN
- NOTILENS_SECRET
bins:
- curl
primaryEnv: NOTILENS_TOKEN
emoji: "🔔"
homepage: https://www.notilens.com
---
# NotiLens — Real-time Alerts
NotiLens delivers real-time push notifications to your phone or team when tasks start, make progress, hit errors, or finish. No polling — instant alerts.
Get your `NOTILENS_TOKEN` and `NOTILENS_SECRET` from your topic settings at https://www.notilens.com.
## Sending a Notification
All notifications are sent as a POST to the NotiLens webhook:
```
POST https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send
X-NOTILENS-KEY: $NOTILENS_SECRET
Content-Type: application/json
```
### Payload Fields
| Field | Type | Required | Description |
|-----------------|---------|----------|-------------|
| `event` | string | yes | Event name, e.g. `task.started`, `task.completed` |
| `title` | string | yes | Short heading shown on the notification |
| `message` | string | yes | Notification body text |
| `type` | string | yes | `info` \| `success` \| `warning` \| `urgent` |
| `agent` | string | yes | Name identifying the source (app, script, agent, etc.) |
| `task_id` | string | no | Task label for grouping related events |
| `run_id` | string | no | Unique ID for this specific run, e.g. `run_1714000000000_a3f2` |
| `is_actionable` | boolean | no | Set `true` when the event needs human attention |
| `image_url` | string | no | Image to display with the notification |
| `open_url` | string | no | URL to open when the notification is tapped |
| `download_url` | string | no | URL of a file to attach to the notification |
| `tags` | string | no | Comma-separated tags, e.g. `prod,backend` |
| `ts` | number | no | Unix timestamp (seconds). Defaults to now. |
| `meta` | object | no | Metrics, counters, timing, and any custom key-value pairs |
## Standard Events and When to Fire Them
Use these canonical event names for consistency across sources:
| Event | `type` | When to fire |
|--------------------|-----------|--------------|
| `task.queued` | `info` | Task is queued before a worker picks it up |
| `task.started` | `info` | Execution begins |
| `task.progress` | `info` | Meaningful checkpoint during a long task |
| `task.paused` | `warning` | Task is pausing (waiting on I/O, rate limit, etc.) |
| `task.waiting` | `warning` | Task is blocked waiting for an external resource |
| `task.resumed` | `info` | Task resumed after a pause or wait |
| `task.loop` | `warning` | Same step is repeating — possible loop |
| `task.retry` | `warning` | Task is being retried after a failure |
| `task.error` | `urgent` | Non-fatal error occurred, task continues |
| `task.completed` | `success` | Task finished successfully |
| `task.failed` | `urgent` | Task failed — will not be retried |
| `task.timeout` | `urgent` | Task exceeded its time limit |
| `task.cancelled` | `warning` | Task was cancelled before completion |
| `task.stopped` | `info` | Task was stopped intentionally (not an error) |
| `task.terminated` | `urgent` | Task was forcibly terminated |
| `input.required` | `warning` | Needs human input to continue |
| `input.approved` | `success` | Human approved the request |
| `input.rejected` | `warning` | Human rejected the request |
| `output.generated` | `success` | Output produced (file, report, result, etc.) |
| `output.failed` | `urgent` | Failed to produce expected output |
You may also use any custom event name appropriate to your workflow (e.g. `order.placed`, `deploy.started`, `pipeline.complete`).
## Metric Tracking
Attach numeric or string metrics to any event's `meta` object. NotiLens surfaces these in the dashboard for filtering and analytics.
### Recommended `meta` Fields
**Timing** (milliseconds):
| Key | Description |
|----------------------|-------------|
| `total_duration_ms` | Wall-clock time from task start to now |
| `active_ms` | Time actively running (excludes pauses and waits) |
| `queue_ms` | Time spent in queue before task started |
| `pause_ms` | Total time spent paused |
| `wait_ms` | Total time spent waiting on external resources |
**Counters**:
| Key | Description |
|----------------|-------------|
| `retry_count` | Number of retries so far |
| `loop_count` | Number of loop iterations |
| `error_count` | Number of non-fatal errors encountered |
| `pause_count` | Number of times the task paused |
| `wait_count` | Number of times the task waited |
**Custom metrics** — include any domain-specific values:
```json
"meta": {
"rows_processed": 4218,
"rows_failed": 3,
"tokens_used": 18400,
"model": "claude-opus-4-6",
"env": "production",
"region": "us-east-1"
}
```
Numeric metrics accumulate meaningfully when charted over time. Include them on `task.completed` and `task.failed` events at minimum.
### `run_id` — Unique Run Identification
Generate a `run_id` at the start of each task run and include it on every event for that run. This allows NotiLens to correlate all events from the same execution even if `task_id` is reused across runs.
```
run_id format: run_{unix_ms}_{random_hex4}
example: run_1714000000000_a3f2
```
## Loop Detection
Fire `task.loop` when the same step is repeating. Include the loop count in `meta`.
```bash
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "task.loop",
"title": "my-app | scraper | task.loop",
"message": "Same page returned 5 times — possible infinite loop.",
"type": "warning",
"agent": "my-app",
"task_id": "scraper",
"run_id": "run_1714000000000_a3f2",
"is_actionable": true,
"meta": {
"loop_count": 5
}
}'
```
## Examples
### Full task lifecycle (queue → start → complete with metrics)
```bash
# 1. Queued
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "task.queued",
"title": "my-app | data-pipeline | task.queued",
"message": "Data pipeline job queued.",
"type": "info",
"agent": "my-app",
"task_id": "data-pipeline",
"run_id": "run_1714000000000_a3f2"
}'
# 2. Started
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "task.started",
"title": "my-app | data-pipeline | task.started",
"message": "Starting nightly data pipeline run.",
"type": "info",
"agent": "my-app",
"task_id": "data-pipeline",
"run_id": "run_1714000000000_a3f2",
"meta": { "queue_ms": 1240 }
}'
# 3. Completed with metrics
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "task.completed",
"title": "my-app | data-pipeline | task.completed",
"message": "Pipeline finished. Processed 4,218 records in 47s.",
"type": "success",
"agent": "my-app",
"task_id": "data-pipeline",
"run_id": "run_1714000000000_a3f2",
"meta": {
"total_duration_ms": 47200,
"active_ms": 45800,
"rows_processed": 4218,
"rows_failed": 0,
"env": "production"
}
}'
```
### Task failed with counters
```bash
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "task.failed",
"title": "my-app | data-pipeline | task.failed",
"message": "Database connection timed out after 3 retries.",
"type": "urgent",
"agent": "my-app",
"task_id": "data-pipeline",
"run_id": "run_1714000000000_a3f2",
"is_actionable": true,
"meta": {
"total_duration_ms": 92000,
"active_ms": 88000,
"retry_count": 3,
"error_count": 3,
"last_error": "connect ETIMEDOUT 10.0.0.5:5432"
}
}'
```
### Pause and resume with timing
```bash
# Pausing (e.g. hit rate limit)
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "task.paused",
"title": "my-app | api-sync | task.paused",
"message": "Rate limit hit — waiting 30s before resuming.",
"type": "warning",
"agent": "my-app",
"task_id": "api-sync",
"run_id": "run_1714000000000_b7c1",
"meta": { "pause_count": 1, "wait_reason": "rate_limit" }
}'
# Resuming
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "task.resumed",
"title": "my-app | api-sync | task.resumed",
"message": "Resuming after rate limit window.",
"type": "info",
"agent": "my-app",
"task_id": "api-sync",
"run_id": "run_1714000000000_b7c1",
"meta": { "pause_ms": 31200, "pause_count": 1 }
}'
```
### Human input required
```bash
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "input.required",
"title": "my-app | approval needed",
"message": "About to delete 83 records. Please confirm.",
"type": "warning",
"agent": "my-app",
"is_actionable": true,
"open_url": "https://dashboard.example.com/approve/123"
}'
```
### Output generated (with download link)
```bash
curl -s -X POST "https://hook.notilens.com/webhook/$NOTILENS_TOKEN/send" \
-H "Content-Type: application/json" \
-H "X-NOTILENS-KEY: $NOTILENS_SECRET" \
-d '{
"event": "output.generated",
"title": "my-app | report-gen | output.generated",
"message": "Monthly report ready. 24 pages, 3 charts.",
"type": "success",
"agent": "my-app",
"task_id": "report-gen",
"download_url": "https://storage.example.com/reports/2026-04.pdf",
"meta": {
"pages": 24,
"charts": 3,
"total_duration_ms": 18400
}
}'
```
## Usage Guidance
- **Always fire `task.started`** when beginning a significant task so the user knows work has begun.
- **Fire `task.completed` or `task.failed`** at every terminal state — never leave a started task without a closing event.
- **Generate a `run_id`** at task start (`run_{unix_ms}_{random_hex4}`) and include it on every event for that run.
- **Include timing in `meta`** on terminal events (`task.completed`, `task.failed`, `task.timeout`) — `total_duration_ms` and `active_ms` at minimum.
- **Include counters in `meta`** whenever they are non-zero: `retry_count`, `error_count`, `loop_count`, `pause_count`, `wait_count`.
- **Use `input.required` with `is_actionable: true`** whenever a human decision is needed before continuing.
- **Fire `task.loop`** when the same logical step is repeating. Include `loop_count` in `meta`.
- **Keep `message` concise and informative** — include counts, durations, or key values (e.g. "Processed 1,240 rows in 3.2s — 2 errors").
- **Use `task_id`** consistently across all events for the same logical task so NotiLens can group them.
- **Do not spam** — avoid sending `task.progress` more than once every few seconds for fast-running tasks.
## Configuration
Set these environment variables before running:
```bash
export NOTILENS_TOKEN=your_topic_token
export NOTILENS_SECRET=your_topic_secret
```
Both are found in your topic settings at https://www.notilens.com.