Implement observability for Lindy AI integrations. Use when setting up monitoring, logging, tracing, or building dashboards for Lindy operations. Trigger with phrases like "lindy monitoring", "lindy...
Monitor workflow health from Lindy's documented task surfaces. Start with Tasks for manual inspection, then use an Agent Task Change trigger followed by Get Task Details for workflow-based monitoring and send only bounded operational fields to an external collector; task inputs, outputs, customer content, and secrets do not belong in metrics or logs.
LINDY_CALLBACK_SECRET by the
receiver and as a protected value in the Lindy HTTP Request actionAuthenticate Lindy's outbound HTTP Request with a dedicated bearer value generated for the metrics receiver. Store it only in Lindy's protected action configuration and the receiver's secret manager, require at least 32 characters, compare it in constant time, and rotate it independently. Never reuse an inbound Lindy webhook secret or a metrics-scrape credential. Export only the three schema fields defined below.
The documented sources for operational signals are:
| Signal | Source | Handling |
|---|---|---|
| Task outcome and frequency | Tasks / Agent Task Change | Aggregate by configured agent key |
| Duration and failing block | Get Task Details | Retain duration; keep block content in Lindy |
| Workspace spend | Lindy billing view | Keep billing data at its documented source |
Create a separate monitoring agent using documented Lindy utilities:
Use Lindy's HTTP Request action to POST the sanitized result. This TypeScript receiver rejects unknown agents, statuses, fields, oversized bodies, invalid durations, and empty secrets:
import { timingSafeEqual } from 'node:crypto';
import express from 'express';
import { Counter, Histogram, Registry } from 'prom-client';
const app = express();
app.use(express.json({ limit: '4kb', strict: true }));
const callbackSecret = process.env.LINDY_CALLBACK_SECRET;
if (!callbackSecret || callbackSecret.trim().length < 32) {
throw new Error('LINDY_CALLBACK_SECRET must contain at least 32 characters');
}
const agentKeys = new Set(
(process.env.LINDY_MONITORED_AGENTS ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean),
);
if (agentKeys.size === 0) throw new Error('LINDY_MONITORED_AGENTS is empty');
type TaskStatus = 'succeeded' | 'failed' | 'canceled';
type MetricInput = { agent: string; status: TaskStatus; durationSeconds: number };
const statuses = new Set<TaskStatus>(['succeeded', 'failed', 'canceled']);
function authorized(header: string | undefined): boolean {
if (!header?.startsWith('Bearer ')) return false;
const actual = Buffer.from(header.slice('Bearer '.length));
const expected = Buffer.from(callbackSecret);
return actual.length === expected.length && timingSafeEqual(actual, expected);
}
function parseMetricInput(value: unknown): MetricInput | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const input = value as Record<string, unknown>;
const allowed = new Set(['agent', 'status', 'durationSeconds']);
if (Object.keys(input).some((key) => !allowed.has(key))) return null;
if (typeof input.agent !== 'string' || !agentKeys.has(input.agent)) return null;
if (typeof input.status !== 'string' || !statuses.has(input.status as TaskStatus)) return null;
if (
typeof input.durationSeconds !== 'number' ||
!Number.isFinite(input.durationSeconds) ||
input.durationSeconds < 0 ||
input.durationSeconds > 86_400
) return null;
return input as MetricInput;
}
const registry = new Registry();
const taskCounter = new Counter<'agent' | 'status'>({
name: 'lindy_tasks_total',
help: 'Total Lindy agent tasks',
labelNames: ['agent', 'status'],
registers: [registry],
});
const taskDuration = new Histogram<'agent'>({
name: 'lindy_task_duration_seconds',
help: 'Lindy task execution duration',
labelNames: ['agent'],
buckets: [1, 2, 5, 10, 30, 60, 120],
registers: [registry],
});
app.post('/lindy/metrics', (req, res) => {
if (!authorized(req.headers.authorization)) return res.sendStatus(401);
const input = parseMetricInput(req.body);
if (!input) return res.status(400).json({ error: 'invalid_metrics_schema' });
taskCounter.inc({ agent: input.agent, status: input.status });
taskDuration.observe({ agent: input.agent }, input.durationSeconds);
// Do not log req.body or task details.
return res.json({ recorded: true });
});
app.get('/metrics', async (_req, res) => {
res.set('Content-Type', registry.contentType);
res.send(await registry.metrics());
});
Configure the HTTP Request action with an allowlisted HTTPS URL, POST, JSON content
type, and Authorization: Bearer <protected callback secret>. Map only:
{
"agent": "support-bot",
"status": "succeeded",
"durationSeconds": 12.4
}
The agent value is a stable configured key, never a task/customer identifier. Do not reuse a secret generated for an inbound Webhook Received trigger as the outbound callback secret.
Use correct counter/histogram aggregation:
| Panel | PromQL |
|---|---|
| Success ratio | sum(rate(lindy_tasks_total{status="succeeded"}[1h])) / clamp_min(sum(rate(lindy_tasks_total[1h])), 1e-9) |
| Failure rate | sum by (agent) (rate(lindy_tasks_total{status="failed"}[15m])) |
| Duration p95 | histogram_quantile(0.95, sum by (le, agent) (rate(lindy_task_duration_seconds_bucket[15m]))) |
| Trigger frequency | sum by (agent) (rate(lindy_tasks_total[15m])) |
Set windows and thresholds from the measured workspace baseline and service objectives. Alert text may link to the task but must not reproduce task content.
Lindy currently documents evals as offline evaluation of selected historical tasks. Use them to compare quality after changes; do not describe them as live monitoring. Keep operational alerts on Tasks/Agent Task Change and quality regression on evals.
| Issue | Response |
|---|---|
| Agent Task Change is silent | Confirm the monitoring agent is active, selected agent is correct, and event is enabled |
| Collector returns 401 | Rotate and update the dedicated callback secret on both sides |
| Collector returns 400 | Reject the event; inspect only field names/types, not payload content |
| Cardinality spike | Restore the configured agent allowlist and remove dynamic labels |
| Dashboard has no samples | Verify HTTP status in the Lindy task and scrape the registry endpoint |
Return an observability plan containing:
LINDY_CALLBACK_SECRET;For a support workflow, select success/failure/canceled events, retrieve task details,
and map only {agent: "support-bot", status: "failed", durationSeconds: 12.4}. The
receiver increments one bounded counter/histogram series. The alert links an operator
to the Lindy task for authorized investigation; it does not copy the customer message
or block output into Slack, logs, or Prometheus.
Hand the verified alert contract and task-link policy to lindy-incident-runbook so
responders can investigate inside Lindy without expanding telemetry data exposure.