Skip to main content
ZDR Signals is an experimental preview. It must be enabled for your organization; organizations without preview access receive 404. Contact support@raindrop.ai to join the preview.
Signals turn examples of an important agent behavior into a classifier that runs on every event you send. Zero data retention is the point of this surface. When you track production events here, Raindrop classifies the full event — data, trace, tool calls — in memory and never stores any of it. What persists is a sanitized identity shell: the event ID, timestamp, an opaque user ID, and the explicit properties you choose to send; events that match a signal additionally record the matched signal IDs. There is one deliberate exception: the labeled training examples you upload to build a signal are retained — that is what the classifier is built and validated against, and you control what goes into them. The retention table below is the complete contract. This guide builds Angry customer, a signal for a support assistant that should catch messages where the customer is angry, frustrated, or threatening to escalate — chargebacks, lawyers, public complaints — while leaving routine questions alone.

Choose how you work

Every step in this guide is available on four surfaces. They drive the same signals, and the results land in the same place.

What you will build

1

Teach the behavior

Upload labeled examples. These are retained — they are what the classifier is built and validated against.
2

Prove it before production

Exercise the active classifier in the Playground or with the synchronous evaluate endpoint. These tests are not stored.
3

Evaluate production events privately

Fire-and-forget up to 1,000 events per batch request. Raw data and trace are evaluated in memory; poll the receipt for per-event results.
4

Review and improve matches

Inspect sanitized event shells in Raindrop — matches carry signal membership — then add edge cases to create a new immutable signal version.

Understand retention first

Training and production have different retention contracts. Use synthetic or approved examples for training, then use ZDR tracking for sensitive production content. batch batches await classification in a memory-only staging buffer: encrypted while queued, capped at 15 minutes, destroyed on read, and never written to disk. Payloads otherwise exist only during classification, for the duration of the run.
properties are part of the persisted event shell. Put only non-sensitive dimensions there, such as product surface, release, or an opaque channel. Keep transcripts, prompts, answers, names, emails, and source URLs inside data or trace.

Before you start

You need:

Set up your surface

Install with one command. The installer prompts for your Organization API Key, downloads the CLI, verifies its checksum, and signs you in:
In CI and other non-interactive environments, set RAINDROP_API_KEY and the installer uses it without prompting:
The CLI requires Node.js 20 or newer and installs to ~/.local/bin/raindrop-signals. To update, re-run the installer.Credentials are saved to ~/.config/raindrop-signals/config.json, so commands work with no environment variables. Each value resolves by precedence flag → RAINDROP_* env var → saved config → default:

Example files

Every --file flag and curl -d @file example in this guide refers to one of the five files below by its exact name. The contents are self-contained fictional support conversations, so you need nothing else to follow along. Labels use exact lowercase values everywhere: match and no_match. Download all five into the current directory:
Re-running the tutorial in the same organization? Give each run unique event IDs so its events don’t attach to a previous run’s stored shells, and send a fresh Idempotency-Key on repeat batch calls (see Make retries safe):
The labeled set that builds the signal: 4 match, 4 no_match, and 1 unlabeled example (label is optional for dataset storage; unlabeled examples do not contribute to validation metrics).
training-events.json
A single event that should classify as match against the built signal.
evaluate-match.json
A single event that should classify as no_match.
evaluate-no-match.json
A ZDR batch of two production events: one matching (angry) and one non-matching (calm).
events-initial.json
Edge cases for a second signal version: sarcasm as match, neutral outage reports as no_match.
refinement-events.json

1. Create the signal

Creating a signal retains the labeled examples (that is the training contract), infers the input/output field mapping, generates a classifier, and scores it against every labeled example. The signal activates automatically only when the validation set has zero false positives, false negatives, and indeterminate outcomes; otherwise the build ends requires_action and you refine with more examples.
One call: the labeled events attach inline, become a retained dataset, and the build is queued. --watch polls the build job until it finishes (usually a minute or two):
Copy the signal_id from the output into a shell variable — later steps use it:
GET /v1/signals/{signalId}/definition (and the CLI’s get and refine --watch output) does not return your dataset events as a separate field, but the generated classifier_code it exposes can reference or quote patterns — sometimes verbatim phrases — from your training examples. Give these responses the same need-to-know handling as the retained dataset itself.
Once active, the signal’s detail page in the dashboard shows matching event volume and three controls: Configure (definition, retained dataset reference, inferred field mapping, immutable versions, generated classifier code, validation metrics), Playground, and Refine.
Configure Signal panel with field mapping and validation metrics

2. Test it before production

Test the active classifier synchronously with payloads that are never stored — nothing from an evaluation request persists, on any surface. outcome is match, no_match, or indeterminate.
A file may hold a single { "data": ... } event or multiple events ({ "events": [...] } or a bare array); the CLI batches them 100 at a time and prints one outcome per event.
Evaluation runs synchronously with an internal 25-second deadline. Under heavy load it can return a 503 with the { "error": { "code": "SERVICE_UNAVAILABLE", ... } } envelope — nothing was persisted, so retry with bounded backoff. The CLI and Local UI retry transient failures for you.

3. Classify production events with ZDR

Use ZDR tracking for production classification. It is fire-and-forget: a request carries one to 1,000 events, is accepted immediately with a receipt, and classification runs asynchronously against every active API-built signal (or an explicit list of up to twenty signal IDs). Poll the receipt for per-event results; sanitized shells land in the Events view whether or not you poll. While a batch waits for classification it exists only in a memory-only staging buffer — encrypted, capped at 15 minutes, never written to disk. The classifier reads it and the staged copy is destroyed. If the buffer is lost before classification runs, the receipt finalizes failed and you resend the batch. Every tracked event persists a sanitized identity shellevent_id, timestamp, user ID, and your explicit properties — whether or not it matches. Matches additionally record the matched signal IDs. data and trace are never stored for any event. Classification reads data and trace, and classifiers can also reference properties as explicit dimensions. The persisted shell identity comes from the top-level event_id, timestamp, and user_id. If timestamp is omitted, the shell records the accept time — the server time when Raindrop received the request — and if user_id is omitted it is anonymous. Because classification runs asynchronously, always send an explicit timestamp for accurate event timelines.
batch submits the batch and prints the receipt ID immediately; --watch polls until the receipt is terminal and prints a summary. Without --watch, check the receipt later — use --json for per-event outcomes:
The CLI fills a missing event_id with a deterministic content hash and splits large files into 1,000-event requests, each with a derived Idempotency-Key, so re-running the same file is idempotent (see Make retries safe).

How long classification takes

Small batches usually reach a terminal status in 5–60 seconds. Classification is queued per organization, so under load a batch can take several minutes — that is normal queueing, not lost data. Use ZDR tracking as designed: fire-and-forget. Submit the batch, then poll the receipt later or read the sanitized shells from the Events view once they appear — do not block a request on a receipt reaching completed. A batch that is not classified within the 15-minute staging window finalizes failed; resend it.

Receipt statuses

  • accepted — the batch is staged and classification is pending; keep polling.
  • completed — the receipt carries the final per-event results and all sanitized identity shells were stored.
  • failed — the batch was not classified: the classifier errored, exhausted its retries, or the staged batch aged past the 15-minute staging window. Resend with a fresh Idempotency-Key.
  • expired — treat the same as failed: resend with a fresh Idempotency-Key.
  • Receipts expire 24 hours after acceptance; after that the endpoint returns 404.
  • An empty matched_signal_ids array is a no-match; the event still stores its sanitized identity shell.
  • indeterminate_signal_ids identifies classifiers that timed out or errored; indeterminate is not a match.
Treat transient 5xx responses and network errors on the receipt GET as retryable, with the same bounded backoff you use for 429 — the receipt is durable for 24 hours, so a failed poll never loses results. A sustained 503 on the batch accept call is staging backpressure: the batch was not accepted, so resend it.

Make retries safe

Send an Idempotency-Key header of at most 256 characters. A repeat request with the same key returns the existing receipt (whatever its status) for 24 hours instead of accepting a duplicate batch. Reuse a key only for the same logical batch; after an expired or failed receipt, resend with a fresh key. The CLI splits batch files into 1,000-event requests automatically and sends a per-request Idempotency-Key derived from the batch content and the signal scope — the same events sent with different --signal-ids get distinct keys (override the base with --idempotency-key), so re-running the same file is retry-safe.

4. Review the results

Tracked-event shells appear in the dashboard’s Events view regardless of which surface sent them; matched events also appear on each matching signal’s detail page.
Read per-event outcomes from the receipt for any batch from the last 24 hours:
To browse the stored shells themselves, use the dashboard’s Events view or GET /v1/events.

5. Refine the decision boundary

Refine when a production match is wrong or a known miss should have matched. Add both the failure and a nearby hard negative whenever possible. The angry-customer scenario refines on sarcasm — anger the first version misses because nothing is shouted:
Each refinement creates an immutable version. The current active version continues serving until the new version activates; a failed refinement does not replace it. base_version is optional but recommended: a stale value returns 409 instead of overwriting a concurrent refinement.
The preview dashboard opens detail controls only for an active API-built signal. If a build returns requires_action, keep the signal_id and job_id from the create response, inspect its metrics through the API or CLI, and submit a refinement there with more labeled examples.

CLI reference

The CLI requires Node.js 20 or newer — install it with the one-command installer. On interactive terminals it renders rich output; pipe it or pass --plain for plain text, or --json for machine-readable output. Input files are JSON: { "events": [...] } objects or bare arrays everywhere, and evaluate also accepts a single-event { "data": ... } file. CSV and spreadsheet mapping is available in the dashboard, not the CLI. Every --file flag also accepts an s3://<bucket>/<key> URI. The object is downloaded on your machine with your own AWS credentials (the standard AWS SDK chain: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars, AWS_PROFILE shared config and SSO profiles, or instance roles) and then sent through the same API call as a local file — Raindrop never sees your bucket or credentials. Any configured AWS_REGION works: bucket region redirects are followed. S3-compatible endpoints (MinIO, LocalStack) are supported via AWS_ENDPOINT_URL_S3.
The CLI reads its saved config automatically. Environment variables and flags override it:
Every command accepts --api-url, --api-key, --project, --json, and --plain. Create, refine, job, batch, and batch-receipt accept --watch. Progress goes to stderr and --json results go to stdout, so pipes remain clean. Exit codes: 0 is success, 1 is a usage or API error, and 2 means a watched outcome went bad — a build job ended failed or requires_action, a tracking receipt ended failed / expired, or the watch deadline expired. batch-receipt also exits 2 for a failed / expired receipt without --watch. The Local UI (ui) mirrors the CLI’s resilience: transient 5xx and 429 responses are retried with capped backoff (honoring Retry-After), and every event row carries its own live status — pending, its match / no-match outcome, or an error badge with the HTTP status — so a blip on one row never sinks the batch.

Event and trace formats

Dataset event

  • data is a required JSON object up to 64 KiB for dataset, evaluate, and ZDR events. Any keys and nesting are accepted; Raindrop infers the field mapping.
  • properties are optional explicit dimensions — the same shape and meaning as ZDR event properties and regular ingest properties. On dataset events they are retained; classifiers see them as event.data.properties.
  • label is match, no_match, or omitted for an unlabeled dataset member.
  • event_id is optional for datasets. When omitted, Raindrop derives a deterministic content hash, making retries and re-uploads predictable.
  • timestamp must be an ISO 8601 datetime and may carry a timezone offset (for example 2026-07-17T09:00:00+02:00); it is normalized to UTC. user_id can be at most 256 characters.

CSV or spreadsheet

The dashboard recognizes common input, output, label, ID, timestamp, user, and trace column names. A minimal CSV looks like this:
Use JSON when your events contain nested objects or traces.

Rich interaction trace

Attach the ordered interaction when the decision depends on retrieval or tool behavior rather than text alone. The trace is a flat ordered list; parent/child relations use span_id / parent_span_id links, the same way spans relate in standard trace exports:
Traces accept at most 500 entries and 64 KiB of JSON. Tool status is pending, completed, error, or unknown; span status is ok, error, or unknown.

Dashboard surface map

When preview mode is enabled, the dashboard focuses its primary navigation on Events, Datasets, and Signals. Existing non-preview behavior remains unchanged otherwise.

API reference

The existing GET /v1/signals endpoint still lists the whole project signal catalog (all signal types). Use the top-level GET /v1/signal-definitions for API-created signals with build state, and GET /v1/signals/{signalId}/definition for one signal’s field mapping, versions, classifier source, and metrics. During the private preview, the request and response schemas for the endpoints above are documented inline in this guide; the public OpenAPI reference at https://query.raindrop.ai/v1/docs documents only the generally available Query API. Standard Query API limits apply: 200 requests per minute per endpoint, 20 requests per second across the API key, and 50 requests per second per IP. Synchronous evaluate is intentionally tighter: 10 requests per minute and 2 requests per second per organization across all signal IDs. Its 429 message directs bulk callers to POST /v1/events/batch. A 429 response means the caller should back off and retry. Transient 5xx responses and network errors — including intermittent 503s on receipt polls, and the SERVICE_UNAVAILABLE-enveloped 503 that evaluate returns past its internal 25-second deadline — are likewise retryable; retry them with bounded backoff. The one exception is a sustained 503 on POST /v1/events/batch, which signals staging backpressure (the batch was not accepted): resend the batch once staging recovers instead of tight-looping. Errors use an { "error": { "code": "...", "message": "..." } } body. Common statuses are 401 for an invalid or missing key, 403 for project access, 404 when preview access or a resource is unavailable, 409 for a stale version or conflicting build, and 429 for rate limits.

Important limits

Build states and troubleshooting

Signal states are building, active, requires_action, and failed. Job states are queued, running, succeeded, requires_action, and failed.
Confirm the preview is enabled for the organization that owns the API key. Feature-disabled requests intentionally return 404.
Check dataset readiness and validation metrics. Add at least one no_match example, then add hard negatives for false positives and positive edge cases for false negatives. The preview dashboard does not open non-active signal details, so use the API or CLI with the original signal_id to submit the refinement.
Another build may be running, or base_version may be stale. Read the signal definition, wait for the current job to finish, and retry from the latest version.
Every tracked event should store a sanitized shell, matched or not. A shell write failure makes the receipt failed; resend the batch with a fresh Idempotency-Key (reusing the same key just returns the existing receipt; shell storage is idempotent per event, so already-written shells dedupe).
That is the ZDR contract. Review the retained event ID, timestamp, opaque user ID, explicit properties, and signal membership. Debug the full payload in your own controlled system using the event ID as the join key.
There is no self-serve delete during the preview. A leftover tutorial signal only affects ZDR batches that run against all active signals — pin signal_ids in production calls and it is inert. Ask support@raindrop.ai to remove signals or retained datasets you no longer want.

Production checklist

  • Use synthetic or explicitly approved data for the retained training dataset.
  • Keep raw conversation content, prompts, answers, and tool results in data or trace.
  • Put only safe operational dimensions in persisted properties.
  • Use stable opaque event and user IDs that you can resolve in your own system.
  • Send an Idempotency-Key for retryable HTTP ingestion.
  • Pin signal_ids when a service should run only a known set of classifiers.
  • Treat indeterminate separately from no_match in operational metrics.
  • Add matched misses and nearby hard negatives together when refining.
  • Use base_version to protect concurrent refinements.