track() API for emitting custom business events that power real-time dashboards.
Automatic Tracing (Agent)
Automatic Tracing (Workflow)
Workflows automatically trace every step with full hierarchy:function, agent, connector, condition, switch, foreach, parallel) is traced with its own color and label in the portal.
Verbose Tracing Mode
Control how much data is saved in traces. Works identically for Agent and Workflow. Modes:full: Complete data including prompts and responses (default)standard: Balanced metadata with truncationminimal: Disables tracing entirely (no traces sent)
Trace Interceptor (onTrace)
Intercept, modify, or cancel traces before they are sent. Available in Agent, Workflow, and standalone logging.- Return the trace (modified or not) to send it
- Return
nullto cancel (trace is not sent) - Return
voidto send unchanged
Trace Hierarchy (startSpan)
Create parent-child relationships between custom logs for structured traces:Custom Executions (Non-Agent Flows)
For scenarios withoutagent.process() (document analysis, batch processing, etc.):
Custom Logging
Log custom events within any execution:Conversation Messages
Available since
@runflow-ai/sdk@1.1.10.message() to record a turn of a conversation. Each call emits a conversation_message trace that the Runflow portal renders as a chat bubble: user inbound on the left, assistant outbound on the right.
The portal switches automatically to chat view when an execution has at least one conversation_message trace — no flag, no channel hint. The thread sidebar preview also updates to show the latest user/assistant text instead of raw envelope JSON.
When to use
- Custom workflows (WhatsApp handlers, webhook routers) where you control the message flow without
agent.process(). - LLM agents when you want to also expose the conversation as chat (wrap
agent.process()calls). - Anywhere you want the execution to render as a conversation in the portal.
message(), nothing changes — your existing traces and rendering keep working exactly as before.
Wrapping an agent call (LLM)
Custom workflow (no LLM)
startSpan call is optional but recommended — it groups the technical traces under the turn so the drill-down drawer in the portal stays organized.
Multiple assistant messages per turn
A turn can emit any number of assistant messages — they render as consecutive bubbles in chronological order, exactly like WhatsApp:Structured content (buttons, audio, image)
content accepts a string OR an object with a type field. The portal renders text natively and falls back to a JSON view for structured content (buttons / media renderers are on the roadmap):
Hierarchy and grouping
Messages follow the same parenting rules aslog() and startSpan():
| You write | Where the message goes |
|---|---|
message({...}) inside a startSpan() block | Child of the active span |
message({..., parentId: span.traceId }) | Child of that span explicitly |
message({...}) with no active span | Root (no grouping) |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
data.role | string | Yes | 'user', 'assistant', 'system', 'tool', or any custom string |
data.content | string | object | Yes | Text (renders natively) or structured object with a type field |
data.metadata | Record<string, any> | No | Extra fields (citations, confidence, custom flags) |
data.parentId | string | No | Explicit parent span’s traceId |
options.parentId | string | No | Same as data.parentId (positional override) |
Business Event Tracking
Usetrack() to emit custom business events from your agent. These events power the Metrics dashboard in the portal, where you can build KPI cards, charts, and real-time feeds without writing any backend code.
Events are buffered and sent in batches automatically (up to 50 events or every 2 seconds). No manual flushing needed during normal execution.
How It Works
- Call
track(eventName, properties)anywhere in your agent code - The SDK buffers events and sends them in batches to the Runflow API
- Open the Metrics tab in the portal to create dashboard cards
- Cards auto-discover your event names and properties — no configuration needed
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
eventName | string | Yes | Name of the event (e.g. 'alert_received', 'order_placed') |
properties | Record<string, any> | No | Key-value pairs with event data |
options | TrackOptions | No | Override threadId, executionId, or timestamp |
Options
Flushing Before Exit
For short-lived scripts or CLI tools, callflushTrackEvents() before exiting to ensure all events are sent:
Dashboard Cards
In the portal, navigate to your agent’s Metrics tab to create cards:- Number — KPI with a single aggregated value (count, sum, avg)
- Rate — Percentage based on a filtered property value
- Line / Bar — Time-series charts grouped by hour, day, week, or month
- Pie — Distribution chart over time periods
| Aggregation | Description | Example |
|---|---|---|
count | Total events | Total alerts received |
rate | Percentage where property matches a value | % of alerts answered |
sum | Sum of a numeric property | Total revenue |
avg | Average of a numeric property | Average response time |
distinct_count | Count of unique property values | Unique companies served |
Best Practices
Use descriptive event names
Use descriptive event names
Use
snake_case names that describe what happened: alert_received, ticket_resolved, payment_processed. Avoid generic names like event or action.Keep properties flat
Keep properties flat
Properties are stored as JSON and queried via keys. Flat key-value pairs work best for dashboard aggregations:
Use consistent property types
Use consistent property types
Keep the same property as the same type across events. If
duration is a number in one event, don’t send it as a string in another — aggregations like sum and avg rely on numeric values.Execution Reviews
Available since
@runflow-ai/sdk@1.1.13. Requires an API client that exposes the reviews namespace.Reviews exposes the execution-review feedback loop programmatically — the same surface used by the portal QA queue and the MCP tools (create_execution_review, list_execution_reviews, …). Use it from LLM-judge agents, KB curators, or scheduled jobs to flag bad executions, triage them, and feed corrected outputs back into training datasets.
checkHasReview first if you don’t want a 409 surfaced to the caller. Authentication uses your RUNFLOW_API_KEY; reviewedBy is auto-populated from the API key label on the backend.
Methods
| Method | Description |
|---|---|
create(args) | Create a review (executionId + agentId + rating + comment ≥ 10 chars). |
checkHasReview(executionId) | Idempotent check before create(). |
list(filters?) | List reviews — filter by agentId, status, rating, priority, dateFrom/dateTo, search (PT full-text). |
get(reviewId) | Fetch a single review. |
update(reviewId, args) | Update status, actionTaken, resolutionNotes, correctedOutput, etc. |
delete(reviewId) | Hard delete. |
stats({ agentId }) | Aggregated counts and avgResolutionHours. |
exportForTraining({ agentId, status?, rating? }) | Export resolved reviews as OpenAI-conversational fine-tuning examples. |
Feedback loop pattern
Errors
Reviews throws typed errors so callers can branch cleanly:
ReviewAlreadyExistsError(HTTP 409) — the execution already has a review.ReviewNotFoundError(HTTP 404) — the reviewId doesn’t exist or belongs to another tenant.ReviewsError(any other status) — generic error withstatus+body.
Reading traces from the SDK (with pagination)
The cross-agent SDK’sExecutions.getDetails(executionId) returns the same hierarchical trace tree the portal shows on the “execution detail” page — but bounded to protect the DB. See Cross-Agent SDK → Executions for the full surface.
Two modes, depending on the caller:
| Caller | Mode | Per-request limit | Why |
|---|---|---|---|
Portal (/api/v1/observability/executions/:id) | Bulk | 10 000 (hard cap) | UI renders aggregations from the trace array. Cap is a safety net — normal traffic never hits it. |
SDK runtime (/api/v1/runtime/v1/observability/executions/:id/details) | Paginated | 500 default, 1 000 max per page | Forces SDK consumers to walk pages instead of pulling everything. |
ObservabilityController.getExecutionDetails keeps returning the entire trace array, with the new pagination fields ignored. The 10 000 cap only kicks in if an execution actually generates that many traces (which would be a bug to investigate, not a regression).
Observability Comparison
| Feature | Agent | Workflow | Standalone (log) |
|---|---|---|---|
| Automatic tracing | Yes | Yes | Manual |
Mode (full/standard/minimal) | Yes | Yes | — |
onTrace interceptor | Yes | Yes | configureLogging() |
| Truncation control | Yes | Yes | — |
| Trace hierarchy | Automatic | Automatic (steps) | startSpan + parentId |
| Chat rendering in portal | Via message() wrapper | Via message() wrapper | Via message() |
| Default mode | full | full | — |
Next Steps
Workflows
Workflow tracing and step types
Configuration
Configure observability