Docs / Strand / tutorials/iot-alert-pipeline

Tutorial: IoT Alert Pipeline

Build an end-to-end pipeline that reads sensor data from an ESP32, sends it through Contact, analyzes it with AI in Strand, and delivers alerts to Slack.

code

ESP32 → Contact (MQTT) → Strand Workflow → AI Analysis → Slack Alert

Time: ~30 minutes Difficulty: Beginner You'll need: An ESP32 board, a Slack workspace, and an OpenAI or Anthropic API key

---

Step 1: Set Up Contact#

Create a Service#

In the Contact dashboard, go to Services and create a new service:

Create an Entity#

Go to EntitiesCreate Entity:

After creation, click Connect to get the MQTT credentials. Save these: you'll need the broker URL, username, and password.

Create an API Key#

Go to Access ControlAPI Keys and create a key for esp32-sensor with the entity:WriteMessages permission. You'll use this in Strand to link the two platforms.

---

Step 2: Program the ESP32#

Flash the following MicroPython code to your ESP32. This reads a temperature sensor and publishes data to Contact every 30 seconds.

python

from umqtt.simple import MQTTClient

# Wi-Fi (connect before this runs)
# Replace with your Contact MQTT credentials
BROKER = "mqtt.tendrl.com"
PORT = 443
CLIENT_ID = "your-account-number:us-1:entity:esp32-sensor"  # your entity's resourcePath
USERNAME = "your-api-key-id"
PASSWORD = "your-api-key-secret"

# ADC pin for temperature sensor (adjust for your hardware)
adc = machine.ADC(machine.Pin(34))
adc.atten(machine.ADC.ATTN_11DB)

def read_temperature():
    """Convert ADC reading to Celsius (adjust for your sensor)."""
    raw = adc.read()
    voltage = raw * 3.3 / 4095
    return round(voltage * 100, 1)  # LM35 formula

client = MQTTClient(CLIENT_ID, BROKER, PORT, USERNAME, PASSWORD, ssl=True)
client.connect()
print("Connected to Contact MQTT broker")

while True:
    temp = read_temperature()
    payload = json.dumps({
        "temperature": temp,
        "unit": "celsius",
        "device": CLIENT_ID
    })
    client.publish("contact/messages", payload)
    print(f"Sent: {temp}°C")
    time.sleep(30)
Tip

Don't have an ESP32? You can simulate messages with curl:

bash

curl -X POST https://app.tendrl.com/api/entities/message \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "msg_type": "publish",
    "data": {
      "temperature": 28.5,
      "unit": "celsius",
      "device": "esp32-sensor"
    }
  }'

---

Step 3: Set Up Strand Connectors#

You need three connectors. Go to the Strand Connectors page and create each one:

Slack Connector#

Field Value
Type Slack
Name My Slack
Bot Token Your Slack bot token (starts with xoxb-)
Default Channel #sensor-alerts
Info

To get a Slack bot token: go to api.slack.com/apps, create an app, add chat:write and chat:write.public scopes under OAuth & Permissions, then install to your workspace. Copy the Bot User OAuth Token.

AI Connector (OpenAI or Anthropic)#

Field Value
Type OpenAI (or Anthropic)
Name My AI
API Key Your OpenAI/Anthropic API key

Contact Message Connector#

Field Value
Type Contact Entity Message
Name Contact Sensor
API Key The entity API key from Step 1

---

Step 4: Create the Strand Workflow#

Go to FlowsNew Workflow and name it Sensor Alert Pipeline.

4a. Add the Trigger#

Your workflow receives events from Contact by tag matching. In the workflow settings:

  1. Turn on Expose to Contact.
  2. Give the workflow one or more trigger tags — for example sensor and temperature.

Any Contact message tagged with all of the workflow's trigger tags starts it automatically; you attach those tags to messages in Contact, via the service or validation action that handles them. No per-workflow wiring.

To trigger the same workflow from your own code instead of a Contact message, call it directly over HTTP — see Trigger a workflow over HTTP.

4b. Add a Python Snippet Node#

Drag a Python Snippet node onto the canvas. This extracts the sensor data from Contact's message format:

python

# Sensor data from the Contact message
sensor_data = payload.get("data", {})

temperature = sensor_data.get("temperature", 0)
device = sensor_data.get("device", "unknown")

# Determine alert level
if temperature > 35:
    alert_level = "critical"
elif temperature > 30:
    alert_level = "warning"
else:
    alert_level = "normal"

return {
    "temperature": temperature,
    "device": device,
    "alert_level": alert_level,
    "needs_analysis": alert_level != "normal"
}

4c. Add a Logic Node (Filter)#

Add a Logic node set to Filter mode. Connect it to the Python Snippet.

Filter code: return payload.get('needs_analysis', False)

Filter mode runs Python, not a Jinja expression, and the upstream node's output arrives as payload.

This ensures we only analyze and alert on abnormal readings, saving AI API costs.

4d. Add an AI Connector Node#

Drag a Connector node and select your AI connector. Connect it after the Logic node.

Operation: chat

System Prompt:

code

You are an IoT monitoring assistant. Analyze sensor readings and provide a brief assessment (2-3 sentences) including: what the reading means, potential causes, and recommended action.

User Message:

code

Device: {{ steps.python_snippet.output_payload.device }}
Temperature: {{ steps.python_snippet.output_payload.temperature }}°C
Alert Level: {{ steps.python_snippet.output_payload.alert_level }}

4e. Add a Slack Connector Node#

Drag another Connector node and select your Slack connector. Connect it after the AI node.

Operation: send_message

Message Text:

code

🌡️ *Sensor Alert: {{ steps.python_snippet.output_payload.alert_level | upper }}*

*Device:* {{ steps.python_snippet.output_payload.device }}
*Temperature:* {{ steps.python_snippet.output_payload.temperature }}°C

*AI Analysis:*
{{ steps.ai_node.output_payload.response }}

Final Workflow#

Your workflow should look like this:

code

[Contact Trigger] → [Python Snippet] → [Logic Filter] → [AI Analysis] → [Slack Alert]

Click Save. There is no separate deploy step: the next trigger uses the version you just saved.

---

Step 5: Test It#

Option A: Send a Test Message from Contact#

bash

curl -X POST https://app.tendrl.com/api/entities/message \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "msg_type": "publish",
    "data": {
      "temperature": 36.2,
      "unit": "celsius",
      "device": "esp32-sensor"
    }
  }'

Option B: Trigger Strand Directly#

Call the workflow by its ID (from the editor URL) — no Contact message needed. The inner data object matches the message shape the Python snippet reads with payload.get("data"):

bash

curl -X POST https://app.tendrl.com/strand/api/workflows/YOUR_WORKFLOW_ID/run \
  -H "Authorization: Bearer YOUR_STRAND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "data": {
        "temperature": 36.2,
        "unit": "celsius",
        "device": "esp32-sensor"
      }
    }
  }'

Check your #sensor-alerts Slack channel; you should see an alert with AI analysis within a few seconds.

---

What's Happening#

  1. ESP32 reads sensor data and publishes via MQTT to Contact
  2. Contact receives the message, validates it against the service rules, and forwards it to Strand via the connector subscription
  3. Strand runs the workflow:
  1. Slack delivers the alert to your team

The entire pipeline runs in seconds with zero custom backend code.

---

Next Steps#