April 3, 2026 Hunter McGuire

Full-Loop Workflows: Device to AI to Device

Two practical workflows that use all three Tendrl products (Contact, Strand, and Surface) in a closed loop. Data flows from a device, through validation, security scanning, and AI processing, then back to the device.

Most IoT platforms handle ingress or processing or delivery. Tendrl does all three: Contact for data ingestion and validation, Strand for workflow automation with AI, and Surface for security scanning. This guide walks through two workflows where data flows from a device, through the full platform, and back to the device.

Both workflows follow the same pattern:

  1. Device → Contact: publish a message with tags. Validation rules check the data at ingress.
  2. Contact → Strand: matching tags trigger a workflow. Surface scans the inbound payload.
  3. Strand (AI): Claude processes with conversation memory, context, and optionally MCP tools.
  4. Strand → Surface: outbound content scanned before delivery.
  5. Strand → Contact → Device: result sent back to the device entity, state table updated.

Validation failures don't just drop the message; they trigger separate workflows that notify ops and command the device to self-diagnose.

AI node capabilities you'll use

Before diving into the workflows, here's a quick primer on the three Strand AI node features that make these loops work:

Conversation memory

Each AI node gets Redis-backed memory, automatically keyed by { account_id }:{ node_id }. Configure memory_ttl (how long history lives) and memory_max_messages (how many messages to keep). On every run, the model loads its history before processing and saves the updated conversation after. A 2-hour TTL with 50 messages gives the AI a rolling window of recent interactions, enough to spot trends without unbounded growth.

Context and templated prompts

The context_md field sets the system prompt: the AI's role, output format, constraints. The prompt field uses Jinja2 templates to inject live data from the workflow: {{ payload.temperature }}, {{ steps.surface_scan.output_payload.threat_level }}. Combined with memory, the AI gets domain expertise (context) + live data (template) + history (memory).

MCP tool use

The mcp_servers field attaches MCP server connectors. The AI model discovers available tools automatically and decides when to call them mid-conversation. An AI node connected to a device database MCP server can look up specs, maintenance history, or threshold configs, then use that information in its response. Tool calls are traced in the output metadata.

Workflow 1: Secure AI Agent for Devices

Full use case page →

Devices send natural language prompts to an AI assistant. Every prompt is validated, scanned for injection, processed with memory and tools, scanned again for data leaks, and delivered back to the device.

Set up the Contact entity

Create an entity for each device (or device type) and attach a service with a DynamicAction for input validation:

DynamicAction: validate AI agent requests
{
  "name": "ai_request_check",
  "requiredFields": ["prompt", "device_id"],
  "rules": [],
  "tags": ["ai-agent"]
}

The requiredFields ensure every message has a prompt and device_id. When validation passes, the ai-agent tag triggers the Strand workflow. When it fails, the failure tags trigger an error workflow.

Build the Strand workflow

The workflow has 6 nodes connected in sequence with a branch after the if/else:

Node 1: Contact trigger. The workflow triggers on messages with tag ai-agent. The full message payload is available as payload.

Node 2: Surface scan (inbound). A surface.platform connector scans the prompt for injection attacks, code extraction, and sensitive data. Uses the Agentic profile (strict sensitive data detection, IP blocking). No API key needed; Strand uses an internal token.

Node 3: If/else on threat level

If/else condition
{{  steps.surface_scan.output_payload.threat_level == "clean"  }}

True branch continues to Claude. False branch sends a rejection back to the device via Contact with the threat details.

Node 4: Claude AI node. This is where all three capabilities come together:

AI node configuration
{
  "model": "claude-sonnet-4-20250514",
  "context_md": "You are a diagnostic assistant for industrial devices. Provide actionable recommendations. Never include credentials or internal URLs in responses.",
  "prompt": "Device: {{  payload.device_id  }}\\nReadings: {{  payload.context  }}\\n\\n{{  payload.prompt  }}",
  "memory_enabled": true,
  "memory_ttl": 7200,
  "memory_max_messages": 50,
  "mcp_servers": ["device-db-server"],
  "temperature": 0.3,
  "max_tokens": 1000
}

The AI sees the system prompt (context_md), the device data injected via Jinja2, its conversation history from Redis, and has access to a device database MCP server for looking up specs and maintenance logs.

Node 5: Surface scan (outbound). Scans Claude's response for credential leaks and PII before delivery.

Node 6: Contact deliver. Sends the AI response back to the device entity via connector.contact_platform with operation message, and patches the state table with update_state.

The error path

When validation fails (missing prompt or device_id), Contact stores the message, injects failure tags, and triggers a separate Strand workflow. That workflow sends the ActionResult back to the device, including which field failed, the actual value, and what was expected. The device SDK can parse this and retry with corrected data.

Device code

MicroPython: send a diagnostic prompt
client.publish(
    data={
        "prompt": "Diagnose high vibration on motor 3",
        "device_id": "motor-ctrl-07",
        "context": {"rpm": 3450, "temp_c": 78.2}
    },
    tags=["ai-agent"]
)

Workflow 2: IoT Anomaly Detection

Full use case page →

Sensor readings are validated at ingress, analyzed by AI with conversation memory for contextual anomaly detection, and alerts are scanned by Surface before reaching dashboards. Invalid data triggers a maintenance workflow.

Validation rules

Attach a service to your sensor entity with a DynamicAction that enforces physical ranges:

DynamicAction: validate sensor readings
{
  "name": "range_check",
  "requiredFields": ["temperature", "humidity", "device_id"],
  "rules": [
    {"field": "temperature", "operator": "between", "value": [-40, 150]},
    {"field": "humidity", "operator": "between", "value": [0, 100]}
  ],
  "tags": ["reading-valid"]
}

Readings outside −40 to 150 °C or 0 to 100 % humidity are caught before they reach the AI. The between operator handles this in a single rule. Validation is passive: the message is always stored, but failed tags route to the maintenance workflow instead of the anomaly detector.

Contextual AI with conversation memory

The AI node is configured with a 2-hour TTL and space for 50 messages. On every sensor reading, the model sees its history of recent readings, a rolling window that captures trends and baseline behavior.

System prompt: anomaly detector
You are an HVAC anomaly detector. You receive sensor
readings over time and maintain memory of recent history.

Analyze the latest reading in context of recent readings.
Flag anomalies: sudden spikes, gradual drifts, unusual
correlations between temperature and humidity.

Return JSON:
{"is_anomaly": bool, "confidence": 0-1,
  "severity": "info|warning|critical",
  "explanation": "..."}
Prompt template: inject live reading
Device: {{  payload.device_id  }}
Temperature: {{  payload.temperature  }}°C
Humidity: {{  payload.humidity  }}%

With 50 messages of history, the model can detect that temperature has been climbing 0.5 degrees per reading for the last hour, or that humidity dropped sharply while temperature stayed flat, patterns that static thresholds would miss.

If the AI flags an anomaly, an if/else node routes to a Surface scan (checking the alert for data leaks), then a Contact connector sends the alert to a dashboard entity and updates the sensor's state table.

The maintenance path

When validation fails (a sensor sends −999 for temperature or omits device_id), the failure tags trigger a separate Strand workflow:

  1. Notify ops: a Contact connector sends the ActionResult to the ops dashboard entity. It includes which field failed, the actual value (−999), the expected range (−40 to 150), and the rule name.
  2. Update device state: a Contact connector with operation update_state sets the sensor's state to needs_maintenance with error details.
  3. Send a tagged diagnostic message: a Contact connector sends a message back to the sensor with tag diagnostic. The device routes it with @client.on(tag="diagnostic").

Bad data isn't silently dropped. It's stored, tagged, routed, and acted on.

Device code

MicroPython: publish a sensor reading
client.publish(
    data={
        "temperature": 74.2,
        "humidity": 45,
        "device_id": "hvac-unit-12"
    },
    tags=["sensor-reading"]
)
MicroPython: route inbound messages
@client.on(tag="diagnostic")
def run_diagnostics(msg):
    client.publish(self_test(), tags=["diagnostic-result"])

@client.on(tag="ai-response")
def handle_ai_reply(msg):
    print(msg.get("data", {}).get("response"))

Patterns across both workflows

What's next

Coming soon Read the docs