I/O Smith connects MQTT, Kafka, HTTP, and databases with declarative flows — JSON transforms, retries, dead-lettering, and full observability built in. No glue code. No boilerplate. Just configuration.
Full engine, every connector, no credit card.
Runs 6 hours a day, free forever.
version: "1.0"
connectors:
mqtt_broker:
type: mqtt
settings:
host: ${MQTT_HOST}
port: 1883
sensor_db:
type: litedb
settings:
database: ${LITEDB_PATH}
flows:
- name: sensor-to-database
trigger:
connector: mqtt_broker
operation: subscribe
params:
topic: sensors/+/data
steps:
- name: filter-valid
type: transform
transform: filter
- name: persist
type: connector
connector: sensor_db
operation: insert
on_error:
strategy: retry
max_retries: 3 version: "1.0"
connectors:
kafka_alerts:
type: kafka
settings:
bootstrap_servers: ${KAFKA_BROKERS}
group_id: iosmith-alert-consumer
webhook:
type: http
settings:
base_url: ${WEBHOOK_BASE_URL}
retry_on_5xx: true
flows:
- name: kafka-critical-alerts
trigger:
connector: kafka_alerts
operation: consume
params:
topic: ${KAFKA_TOPIC}
steps:
- name: filter-critical
type: transform
transform: filter
- name: forward-to-webhook
type: connector
connector: webhook
operation: post
params:
path: /alerts
on_error:
strategy: skip Every plant, broker, and legacy API gets its own microservice: a consumer loop, some JSON mangling, retry logic you half-tested, and logging you'll regret. Multiply that by every integration and you're maintaining a fleet of near-identical services. I/O Smith replaces them with one engine and one config file.
Write connectors and flows in YAML 1.2. Schema-validated at startup with line/column errors — fails fast, CI-friendly, exits non-zero before touching any data.
Built-in connectors: MQTT v3.1.1/v5, Kafka, HTTP/REST webhooks, and LiteDB. Declared once under connectors, shared across flows, with lifecycle and reconnection managed by the engine.
Each flow runs as an isolated, backpressured channel pipeline with per-flow concurrency. One flow failing never takes down another.
Expose flows as HTTP endpoints or call external APIs as a step. Supports GET, POST, custom headers, and auth — declared in YAML, no server code required.
map, filter, set_field take full JSON expressions over payload, headers, and trace context. Compiled once, cached, timeout-bounded. Syntax errors caught at startup, not at 3 a.m.
retry with backoff, dead_letter to any connector, or skip — declared per flow in YAML. No try/catch pyramids.
Per-flow throughput, failure counters by error type, P50/P95/P99 step latency, queue depth, connector status. W3C trace context on every envelope; one correlation ID finds every log line a message ever touched.
Step parameters resolve template expressions against the live message — payload fields, headers, trigger metadata, timestamps, UUIDs. Secrets and config via ${ENV_VAR} placeholders, validated at startup.
Secrets only via ${ENV_VAR} — hardcoded credentials are rejected at validation. TLS on all network connectors. Parameterized DB queries, never string interpolation.
Standalone binary, Docker container, or Kubernetes with config via ConfigMap and secrets via Secrets. Starts processing within 5 seconds.
version: "1.0"
connectors:
mqtt_broker:
type: mqtt
settings:
host: ${MQTT_HOST}
port: 1883
client_id: iosmith-sensor-listener
qos: 1
username: ${MQTT_USER}
password: ${MQTT_PASSWORD}
sensor_db:
type: litedb
settings:
database: ${LITEDB_PATH}
collection: sensor_readings
flows:
- name: sensor-to-database
description: Receives MQTT sensor readings and persists them to LiteDB
concurrency: 2
trigger:
connector: mqtt_broker
operation: subscribe
params:
topic: sensors/+/data
steps:
- name: decode-json
type: transform
transform: json_decode
- name: filter-valid
type: transform
transform: filter
with:
condition: '$.payload.sensorId != "" and $.payload.value != null and $.payload.machineId != "606ba38d-2979-4a91-a76d-2415e011d1fd"'
- name: map-envelope
type: transform
transform: map
with:
expression: '{"sensorId": $.payload.sensorId, "value": $.payload.value, "unit": $.payload.unit}'
- name: add-timestamp
type: transform
transform: set_field
with:
field: received_at
value: '$now()'
- name: add-topic
type: transform
transform: set_field
with:
field: mqtt_topic
value: '$.headers.topic'
- name: persist
type: connector
connector: sensor_db
operation: insert
params:
collection: sensor_readings
on_error:
strategy: retry
max_retries: 3
retry_delay_ms: 1000 MQTT sensor readings persisted to LiteDB
decode-json Parse raw MQTT payload as JSON filter-valid Drop malformed or blocked sensor readings map-envelope Reshape to canonical sensor schema add-timestamp Stamp ingestion time via $now() add-topic Carry MQTT topic into the record persist Insert into LiteDB sensor_readings collection version: "1.0"
connectors:
kafka_alerts:
type: kafka
settings:
bootstrap_servers: ${KAFKA_BROKERS}
group_id: iosmith-alert-consumer
auto_offset_reset: latest
sasl_mechanism: PLAIN
sasl_username: ${KAFKA_USER}
sasl_password: ${KAFKA_PASSWORD}
security_protocol: SASL_SSL
webhook:
type: http
settings:
base_url: ${WEBHOOK_BASE_URL}
timeout_ms: 5000
headers:
Authorization: "Bearer ${WEBHOOK_TOKEN}"
retry_on_5xx: true
flows:
- name: kafka-critical-alerts
description: Forwards critical Kafka events to an HTTP webhook
concurrency: 1
trigger:
connector: kafka_alerts
operation: consume
params:
topic: ${KAFKA_TOPIC}
steps:
- name: decode-json
type: transform
transform: json_decode
- name: filter-critical
type: transform
transform: filter
with:
condition: "{{payload.severity}}"
- name: forward-to-webhook
type: connector
connector: webhook
operation: post
params:
path: /alerts
on_error:
strategy: skip Critical Kafka alerts forwarded to an HTTP webhook with retry
decode-json Deserialize Kafka message payload filter-critical Pass only messages where severity is set forward-to-webhook POST to webhook with automatic retry on 5xx version: "1.0"
connectors:
http_ingress:
type: http_ingress
settings:
port: ${HTTP_PORT}
path: /commands
mqtt_broker:
type: mqtt
settings:
host: ${MQTT_HOST}
port: 1883
client_id: iosmith-command-publisher
qos: 1
username: ${MQTT_USER}
password: ${MQTT_PASSWORD}
flows:
- name: http-to-device-command
description: Accepts HTTP POST device commands and publishes them to the MQTT command topic
concurrency: 4
trigger:
connector: http_ingress
operation: listen
steps:
- name: decode-json
type: transform
transform: json_decode
- name: filter-valid-command
type: transform
transform: filter
with:
condition: '$.payload.deviceId != "" and $.payload.command != null'
- name: map-command
type: transform
transform: map
with:
expression: '{"deviceId": $.payload.deviceId, "command": $.payload.command, "params": $.payload.params}'
- name: add-dispatched-at
type: transform
transform: set_field
with:
field: dispatched_at
value: '$now()'
- name: add-trace-id
type: transform
transform: set_field
with:
field: traceId
value: '$.trace.traceId'
- name: encode-json
type: transform
transform: json_encode
- name: publish-command
type: connector
connector: mqtt_broker
operation: publish
params:
topic: devices/commands
on_error:
strategy: skip HTTP POST device commands fanned out to MQTT broker
decode-json Parse incoming HTTP request body filter-valid-command Reject commands missing deviceId or command map-command Normalize to device command schema add-dispatched-at Stamp dispatch time add-trace-id Carry W3C trace ID into the MQTT payload encode-json Serialize back to JSON for MQTT publish publish-command Publish to devices/commands topic
Every example above ships with the trial download and runs with iosmith run flow.yaml.
Metrics sourced from the engine specification. Benchmark methodology will be published when available.
Cells marked ⭐ are dimensions where that alternative outperforms I/O Smith. We'd rather help you pick the right tool than win a table.
| Dimension | I/O Smith | Node-RED | Kafka Connect | Custom microservices |
|---|---|---|---|---|
| Config format | YAML file — git-trackable, diffable, reviewable | Visual graph (JSON underneath — noisy diffs) | JSON / properties files | Code in any language |
| Code-reviewable | Yes — PR diff is the config diff | Limited — graph exports are hard to review | Yes | Yes |
| Industrial protocols | MQTT, Kafka, HTTP, LiteDB (built in) | Very broad — 300+ community nodes | Kafka ecosystem only | Unlimited — write it yourself |
| Ops burden | One binary, one config file, one metrics endpoint | Node-RED server + flow management UI to operate | Kafka cluster + Connect workers to operate | Per-service: CI, deploy, monitor, scale |
| Best suited for | Declarative multi-protocol integration without writing code | ⭐ Visual prototyping and low-code / citizen-developer authoring | ⭐ Pure Kafka pipelines — 100s of community connectors | ⭐ Arbitrary protocol support or bespoke business logic |
Full engine · Free forever
Unlimited runtime · Talk to us for pricing
Trigger connectors pause and in-flight messages are dropped — there is no delivery guarantee at the cap. Do not run the free tier against data you cannot afford to lose. Processing resumes automatically at the daily reset (00:00 UTC), or instantly when you activate a license key — no redeployment needed.
Yes: the counter tracks engine runtime, not wall-clock time. Stop the engine, stop the clock. Restarting the engine does not reset the daily budget — the counter is persisted locally so process restarts cannot circumvent the cap.
Drop your license key into the same deployment via an environment variable or config file. The engine validates it at runtime and lifts the cap immediately — no redeploy, no message-state carryover. Flows restart fresh from your existing YAML config.
docker run -v ./flow.yaml:/app/flow.yaml iosmith/engine Copy a bundled sample, point it at your broker, watch the metrics endpoint light up — with 5½ hours of today's budget to spare.
Tell us what you're integrating. We'll get back to you within 24 hours.