This guide explains how to run the Magistrala Agent with Node-RED support using a mock Linux device in Docker.
make all && make dockers_devmake runThis starts: Agent + UI (:9999), Node-RED (:1880).
# Check agent health
curl http://localhost:9999/health
# Ping Node-RED via agent
curl -s -X POST http://localhost:9999/nodered \
-H 'Content-Type: application/json' \
-d '{"command":"nodered-ping"}'
# Open the Agent UI
open http://localhost:9999
# Open Node-RED UI
open http://localhost:1880If you have a running Atom instance, use the provisioning script to automatically create entities, resources, and bootstrap profiles:
export MG_AGENT_BOOTSTRAP_EXTERNAL_ID="<device-external-id>"
export MG_AGENT_BOOTSTRAP_EXTERNAL_KEY="<device-external-key>"
export MG_PAT="<personal-access-token>"
export MG_TENANT_ID="<tenant-id>"
make run_provisionUse your real PAT in the shell, but do not commit it to files. The agent and Node-RED do not consume a generated config.toml; they fetch the rendered bootstrap profile at startup.
The PAT used for provisioning must be able to create bootstrap configs, rules, devices, and channels in the target tenant. The script expects scopes such as:
bootstrap:createrules:creategateways:creategateways:viewgateways:connect_to_channelchannels:createchannels:viewchannels:connect_client
Or with a custom API URL:
export MG_API=https://my-instance/api
export MG_AGENT_BOOTSTRAP_EXTERNAL_ID="<device-external-id>"
export MG_AGENT_BOOTSTRAP_EXTERNAL_KEY="<device-external-key>"
export MG_TENANT_ID="<tenant-id>"
export MG_PAT="<pat>"
make run_provisionFor Atom Cloud specifically, use:
export MG_API=https://cloud.magistrala.absmach.eu/api
export MG_AGENT_MQTT_URL=ssl://messaging.magistrala.absmach.eu:8883
export MG_AGENT_MQTT_SKIP_TLS=false
export MG_AGENT_BOOTSTRAP_EXTERNAL_ID="<device-external-id>"
export MG_AGENT_BOOTSTRAP_EXTERNAL_KEY="<device-external-key>"
export MG_TENANT_ID="<tenant-id>"
export MG_PAT="<pat>"
make run_provisionThat combination targets the cloud APIs for provisioning and the cloud MQTT broker for runtime messaging.
Or run the script directly:
export MG_AGENT_BOOTSTRAP_EXTERNAL_ID="<device-external-id>"
export MG_AGENT_BOOTSTRAP_EXTERNAL_KEY="<device-external-key>"
export MG_PAT="<personal-access-token>"
export MG_TENANT_ID="<tenant-id>"
bash scripts/provision.shThis will:
- Register a Gateway with credentials
- Create telemetry and commands Channels
- Create a Bootstrap Profile and Enrollment with
external_idandexternal_key - Bind the profile slots to the provisioned gateway and channels
- Configure a Rule Engine rule with
save_senmloutput for telemetry messages
Alternatively, if you prefer to set up resources manually via the Atom UI or API, create the entity, telemetry channel, commands channel, bootstrap profile, enrollment, bindings, and rules, then edit docker/.env with the bootstrap values:
MG_AGENT_BOOTSTRAP_URL=http://bootstrap:9013/clients/bootstrap
MG_AGENT_BOOTSTRAP_EXTERNAL_ID=<external-id>
MG_AGENT_BOOTSTRAP_EXTERNAL_KEY=<external-key>Then restart the agent:
docker compose up -dOpen http://localhost:9999 in a browser. The Node-RED panel lets you:
- Ping — check that Node-RED is reachable
- State — get the current runtime state
- Get Flows — fetch and inspect the deployed flows (pretty-printed JSON)
- Select JSON File — pick a local
.jsonflow file - Deploy Flows — replaces all currently running flows in Node-RED with the selected file. Any flows that were running before will be stopped and removed.
- Add Flow — adds the selected file as a new flow tab alongside existing flows without removing anything that is already running.
All responses are shown inline with the command label. The file is base64-encoded in the browser before being sent to the agent.
# Deploy a flow (flow JSON must be base64 encoded)
FLOWS=$(cat examples/nodered/speed-flow.json | base64 -w 0)
curl -s -X POST http://localhost:9999/nodered \
-H 'Content-Type: application/json' \
-d "{\"command\":\"nodered-deploy\",\"flows\":\"$FLOWS\"}"
# Add a flow tab (non-destructive, flow JSON must be base64 encoded)
curl -s -X POST http://localhost:9999/nodered \
-H 'Content-Type: application/json' \
-d "{\"command\":\"nodered-add-flow\",\"flows\":\"$FLOWS\"}"
# Fetch current flows
curl -s -X POST http://localhost:9999/nodered \
-H 'Content-Type: application/json' \
-d '{"command":"nodered-flows"}'
# Ping Node-RED
curl -s -X POST http://localhost:9999/nodered \
-H 'Content-Type: application/json' \
-d '{"command":"nodered-ping"}'
# Get flow state
curl -s -X POST http://localhost:9999/nodered \
-H 'Content-Type: application/json' \
-d '{"command":"nodered-state"}'Send a SenML array to m/<tenant-id>/c/<commands-channel-id>/req:
Supported commands:
nodered-deploy,<base64-flow>— Replace all running flows with the provided flow JSONnodered-add-flow,<base64-flow>— Add a new flow tab alongside existing running flowsnodered-flows— Fetch current flowsnodered-state— Get runtime statenodered-ping— Check Node-RED availability
[
{
"bn": "uuid:",
"n": "control",
"vs": "nodered-deploy,<base64-encoded-flow-json>"
}
]Seeded into Node-RED on first start. It periodically publishes SenML temperature and humidity readings to the Atom data channel.
A ready-to-deploy example that publishes speed (km/h), rpm, and gear SenML records every 15 seconds. Use it to test remote flow deployment end-to-end.
Simulates polling 4 Modbus TCP holding registers (FC03) every 10 seconds and publishing SenML records to Magistrala:
| Register | Measurement | Unit |
|---|---|---|
| HR0 | Voltage | V |
| HR1 | Current (scaled ×10) | A |
| HR2 | Power | W |
| HR3 | Temperature | °C |
The simulation function node can be replaced with a real modbus-read node when a physical Modbus TCP slave is available.
Deploy via Magistrala MQTT:
# 1. Encode the flow
FLOWS=$(cat examples/nodered/speed-flow.json | base64 -w 0)
# 2. Publish the deploy command
mosquitto_pub \
-h <mqtt-host> -p 8883 \
--capath /etc/ssl/certs \
-I "agent-mock-device" \
-u <gateway-id> -P <gateway-secret> \
-t "m/<tenant-id>/c/<commands-channel-id>/req" \
-m "[{\"bn\":\"req-1:\",\"n\":\"nodered\",\"vs\":\"nodered-deploy,$FLOWS\"}]"The agent will:
- Receive the SenML message over MQTT
- Base64-decode the flow JSON
- Patch the MQTT
gatewayidin the flow to<gateway-id>-nr(prevents session conflict with the agent itself) POSTthe flows to Node-RED's REST API- Publish the result back to the control channel
Note on
gatewayidvsclientid: The agent sets a customgatewayidproperty onmqtt-brokernodes. Node-RED's standardmqtt-brokernode usesclientidfor the MQTT client ID. The custom Node-RED entrypoint (docker/nodered/entrypoint.js) readsgatewayidat startup and maps it to the standardclientidfield. This prevents MQTT session conflicts between the agent and its deployed Node-RED flows.
Verify the deployment:
mosquitto_pub \
-h <mqtt-host> -p 8883 \
--capath /etc/ssl/certs \
-I "agent-mock-device" \
-u <gateway-id> -P <gateway-secret> \
-t "m/<tenant-id>/c/<commands-channel-id>/req" \
-m '[{"bn":"req-2:", "n":"nodered", "vs":"nodered-flows"}]'The agent logs will show:
{
"level": "INFO",
"msg": "NodeRed command \"nodered-deploy,...\" completed successfully.",
"duration": "...",
"uuid": "req-1"
}Node-RED will start publishing speed data to the telemetry topic within 3 seconds of deployment. The agent normalises all m/.../c/.../data and m/.../c/.../gateway/telemetry topics in the flow to the rendered telemetry channel: m/<tenant-id>/c/<telemetry-channel-id>/gateway/telemetry.
The Node-RED URL is configured via environment variable:
- Environment variable:
MG_AGENT_NODERED_URL(default:http://localhost:1880/)
Device identity, MQTT credentials, tenant ID, and telemetry/commands channel IDs come from the rendered bootstrap profile.
| Direction | Topic | QoS | Description |
|---|---|---|---|
| Cloud → Agent | m/<tenant-id>/c/<ctrl-chan>/req |
1 | Node-RED commands via nodered or control subsystem |
| Agent → Cloud | m/<tenant-id>/c/<ctrl-chan>/res |
1 | Command response |
| Node-RED → Cloud | m/<tenant-id>/c/<telemetry-chan>/gateway/telemetry |
0 | SenML telemetry published by deployed flows |
All recipes use the commands channel request topic: m/<tenant-id>/c/<commands-channel-id>/req.
Responses are published to: m/<tenant-id>/c/<commands-channel-id>/res.
mosquitto_sub \
-h <mqtt-host> -p 1883 \
-u <gateway-id> -P <gateway-secret> \
-t "m/<tenant-id>/c/<commands-channel-id>/res" \
-vmosquitto_pub \
-h <mqtt-host> -p 1883 \
-u <gateway-id> -P <gateway-secret> --id "nr-$(date +%s)" \
-t "m/<tenant-id>/c/<commands-channel-id>/req" \
-m '[{"bn":"req-1:", "n":"nodered", "vs":"nodered-ping"}]'Expected response:
[
{
"bn": "req-1",
"bt": 1781261269.1195753,
"n": "nodered",
"vs": "{\"httpNodeRoot\":\"/\",\"version\":\"4.1.10\",\"context\":{\"default\":\"memory\",\"stores\":[\"memory\"]},\"libraries\":[{\"id\":\"local\",\"label\":\"editor:library.types.local\",\"user\":false,\"icon\":\"font-awesome/fa-hdd-o\"},{\"id\":\"examples\",\"label\":\"editor:library.types.examples\",\"user\":false,\"icon\":\"font-awesome/fa-life-ring\",\"types\":[\"flows\"],\"readOnly\":true}],\"flowEncryptionType\":\"disabled\",\"diagnostics\":{\"enabled\":true,\"ui\":true},\"telemetryEnabled\":false,\"runtimeState\":{\"enabled\":false,\"ui\":false},\"functionExternalModules\":true,\"functionTimeout\":0,\"tlsConfigDisableLocalFiles\":false,\"editorTheme\":{\"projects\":{\"enabled\":false},\"languages\":[\"de\",\"en-US\",\"es-ES\",\"fr\",\"ja\",\"ko\",\"pt-BR\",\"ru\",\"zh-CN\",\"zh-TW\"]}}"
}
]mosquitto_pub \
-h <mqtt-host> -p 1883 \
-u <gateway-id> -P <gateway-secret> --id "nr-$(date +%s)" \
-t "m/<tenant-id>/c/<commands-channel-id>/req" \
-m '[{"bn":"req-1:", "n":"nodered", "vs":"nodered-state"}]'Expected response:
[
{
"bn": "req-1",
"bt": 1781865951.7412772,
"n": "nodered",
"vs": "{\"state\":\"start\"}"
}
]mosquitto_pub \
-h <mqtt-host> -p 1883 \
-u <gateway-id> -P <gateway-secret> --id "nr-$(date +%s)" \
-t "m/<tenant-id>/c/<commands-channel-id>/req" \
-m '[{"bn":"req-1:", "n":"nodered", "vs":"nodered-flows"}]'Expected response (after deploying speed-flow.json, truncated):
[
{
"bn": "req-1",
"bt": 1781865982.932532,
"n": "nodered",
"vs": "[{\"id\":\"flow-speed-sensor\",\"type\":\"tab\",\"label\":\"Speed Sensor Flow\",\"disabled\":false,\"info\":\"Publishes SenML speed data to Magistrala cloud every 15s via MQTT over TLS.\"},{\"id\":\"mqtt-broker-config\",\"type\":\"mqtt-broker\",\"name\":\"Magistrala Cloud MQTT\",\"broker\":\"host.docker.internal\",\"port\":\"8883\",\"clientid\":\"<client-id>-nr\",\"usetls\":true,\"tls\":\"magistrala-agent-tls\",\"credentials\":{\"user\":\"<client-id>\",\"password\":\"<client-secret>\"},\"z\":\"\"},{\"id\":\"inject-speed\",\"type\":\"inject\",\"z\":\"flow-speed-sensor\",\"name\":\"Every 15s\",\"repeat\":\"15\",\"once\":true,\"onceDelay\":3,\"wires\":[[\"build-speed-senml\"]]},{\"id\":\"build-speed-senml\",\"type\":\"function\",\"z\":\"flow-speed-sensor\",\"func\":\"var now = Date.now() * 1e6;\\nmsg.payload = JSON.stringify([\\n {\\\"bn\\\": \\\"speed-sensor:\\\", \\\"bt\\\": now, \\\"n\\\": \\\"speed\\\", \\\"u\\\": \\\"km/h\\\", \\\"v\\\": 60 + Math.random() * 40}\\n]);\\nmsg.topic = \\\"m/<tenant-id>/c/<telemetry-channel-id>/gateway/telemetry\\\";\\nreturn msg;\",\"wires\":[[\"mqtt-pub-speed\",\"debug-speed\"]]}]"
}
]Note the normalisations the agent applied: the MQTT client ID (gatewayid — mapped to Node-RED's standard clientid by the custom entrypoint.js) suffixed with -nr, usetls set to true, tls set to magistrala-agent-tls, and the topic rewritten to m/<tenant-id>/c/<telemetry-channel-id>/gateway/telemetry.
FLOWS=$(cat examples/nodered/speed-flow.json | base64 -w 0)
mosquitto_pub \
-h <mqtt-host> -p 1883 \
-u <gateway-id> -P <gateway-secret> --id "deploy-$(date +%s)" \
-t "m/<tenant-id>/c/<commands-channel-id>/req" \
-m "[{\"bn\":\"req-1:\",\"n\":\"nodered\",\"vs\":\"nodered-deploy,$FLOWS\"}]"Expected response (Node-RED returns 204 No Content on successful full deploy):
[
{
"bn": "req-1",
"bt": 1781865982.932532,
"n": "nodered",
"vs": ""
}
]Warning:
nodered-deployreplaces all running flows. Usenodered-add-flowto add without replacing.
FLOWS=$(cat examples/nodered/modbus-flow.json | base64 -w 0)
mosquitto_pub \
-h <mqtt-host> -p 1883 \
-u <gateway-id> -P <gateway-secret> --id "addflow-$(date +%s)" \
-t "m/<tenant-id>/c/<commands-channel-id>/req" \
-m "[{\"bn\":\"req-1:\",\"n\":\"nodered\",\"vs\":\"nodered-add-flow,$FLOWS\"}]"Expected response:
[
{
"bn": "req-1",
"bt": 1781866008.2599385,
"n": "nodered",
"vs": "{\"id\":\"53c5e22d806c0d04\"}"
}
]The response contains the new flow tab ID. Node IDs in the submitted flow are rekeyed (replaced with fresh nr- prefixed IDs) before being sent to Node-RED, so example flows with fixed IDs can be added alongside existing flows without duplicate-ID conflicts.
After deploying a flow, subscribe to the telemetry topic to confirm Node-RED is publishing SenML data:
mosquitto_sub \
-h <mqtt-host> -p 1883 \
-u <gateway-id> -P <gateway-secret> --id "tel-$(date +%s)" \
-t "m/<tenant-id>/c/<telemetry-channel-id>/gateway/telemetry" \
-vExpected output (speed-flow.json, every 15s):
m/<tenant-id>/c/<telemetry-channel-id>/gateway/telemetry [{"bn":"speed-sensor:","bt":1781866061271000000,"n":"speed","u":"km/h","v":82.4},{"n":"rpm","u":"rpm","v":1923},{"n":"gear","u":"1","v":3}]
Expected output (modbus-flow.json, every 10s):
m/<tenant-id>/c/<telemetry-channel-id>/gateway/telemetry [{"bn":"modbus-device:","bt":1781866061271000000,"n":"hr0","u":"V","v":236},{"n":"hr1","u":"A","v":11.2},{"n":"hr2","u":"W","v":2643},{"n":"hr3","u":"Cel","v":48}]
Unknown commands and missing flow arguments return an error response on the response topic:
# Unknown command
mosquitto_pub ... -m '[{"bn":"req-1:", "n":"nodered", "vs":"nodered-unknown"}]'
# Response: [{"bn":"req-1","n":"nodered","vs":"failed to execute node-red operation : Unknown command"}]
# Deploy without flow argument
mosquitto_pub ... -m '[{"bn":"req-1:", "n":"nodered", "vs":"nodered-deploy"}]'
# Response: [{"bn":"req-1","n":"nodered","vs":"invalid command"}]
# Invalid base64 payload
mosquitto_pub ... -m '[{"bn":"req-1:", "n":"nodered", "vs":"nodered-deploy,%%%bad"}]'
# Response: [{"bn":"req-1","n":"nodered","vs":"failed to execute node-red operation : ..."}]Node-RED commands can also be sent via the control subsystem:
FLOWS=$(cat examples/nodered/speed-flow.json | base64 -w 0)
mosquitto_pub \
-h <mqtt-host> -p 1883 \
-u <gateway-id> -P <gateway-secret> --id "ctrl-$(date +%s)" \
-t "m/<tenant-id>/c/<commands-channel-id>/req" \
-m "[{\"bn\":\"req-1:\",\"n\":\"control\",\"vs\":\"nodered-deploy,$FLOWS\"}]"| Variable | Default | Description |
|---|---|---|
MG_AGENT_NODERED_URL |
http://localhost:1880/ |
Node-RED REST API base URL |
MG_AGENT_BOOTSTRAP_URL |
http://bootstrap:9013/clients/bootstrap |
Bootstrap fetch URL |
MG_AGENT_BOOTSTRAP_EXTERNAL_ID |
Bootstrap external ID | |
MG_AGENT_BOOTSTRAP_EXTERNAL_KEY |
Bootstrap external key |