Skip to Content
IoT App DevelopmentIronFlock SDK

IronFlock SDK

The IronFlock SDK lets your edge applications interact with the IronFlock platform. It handles authentication automatically when running on a registered device and provides functions for publishing data, querying history, calling remote procedures across devices, and updating device metadata.

SDKPackageRequires
Pythonironflock on PyPIPython 3.8+
JavaScriptironflock on npmNode.js 18+ or modern browser

Installation

pip install ironflock

Or add ironflock to your app’s requirements.txt.

Quick Start

import asyncio from ironflock import IronFlock async def main(): while True: await ironflock.publish_to_table("sensordata", { "temperature": 22.5, "humidity": 60 }) await asyncio.sleep(5) ironflock = IronFlock(mainFunc=main) ironflock.run()

When used inside an IronFlock app container, the SDK reads connection credentials from the environment automatically — no manual configuration needed.

Constructor Options

ironflock = IronFlock( mainFunc=main, # async function to run after connecting serial_number="abc123" # override device serial (optional) )
ParameterDescription
mainFuncAn async function that runs once the connection is established
serial_numberOverride the device serial number. Defaults to DEVICE_SERIAL_NUMBER env var

Publishing Data

publishToTable / publish_to_table

Publishes a data record to a fleet table. The table name must match a table defined in your app’s data-template.yml. The SDK automatically routes the data to the correct project database.

await ironflock.publish_to_table("sensordata", { "temperature": 22.5, "humidity": 60, "device_id": "sensor-001" })

appendToTable / append_to_table

Appends data to a fleet table using a remote procedure call instead of pub/sub. Use this when you need confirmation that the data was persisted.

result = await ironflock.append_to_table("sensordata", { "temperature": 22.5, "humidity": 60 })

publishRowsToTable / publish_rows_to_table

Publishes many rows in a single message (bulk insert) to a fleet table. The platform inserts the whole batch atomically (all-or-nothing) in one operation. Use this for high-frequency data where one round-trip per row would be too costly. Like publishToTable, this is fire-and-forget — the acknowledgement confirms delivery to the router, not the database insert.

await ironflock.publish_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ])

The second argument is a non-empty list of row objects to insert.

appendRowsToTable / append_rows_to_table

Appends many rows in a single remote procedure call (bulk insert) to a fleet table. The platform inserts the whole batch atomically (all-or-nothing): if any row is invalid the entire batch is rejected and nothing is persisted. Prefer this over publishRowsToTable / publish_rows_to_table when you need the insert outcome.

result = await ironflock.append_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ]) # result -> {"success": True, "count": 2}

reportError / report_error

Reports an application error into your fleet’s error-logs table. This is a convenience wrapper over publishToTable / appendToTable: it stamps the row with source: "app", a severity level, and a timestamp, then writes it like any normal table row. The error lands in the same error-logs table that fleetdb system errors use (tagged source: "system"), so it is queryable with getHistory, streamable with subscribeToTable / subscribe_to_table, usable in board-templates, and delivered in realtime on transformed.error-logs — without firing the platform’s system-error toast.

# Fire-and-forget (default): publishes to the error-logs table await ironflock.report_error("Sensor read timed out", level="warn") # Pass an exception to capture its traceback (falls back to the message) try: risky_operation() except Exception as err: await ironflock.report_error(err) # Use the append RPC when you want to await the insert outcome await ironflock.report_error("Calibration failed", level="error", append=True)

Parameters:

ParameterTypeDescription
errorstr / string or exception / ErrorThe error message, or an exception whose traceback/stack (or message) is recorded
levelstr / string, optionalSeverity: "error", "warn", "info" or "debug". Defaults to "error"
appendbool / boolean, optionalWhen true, use the append RPC (returns the insert outcome). Defaults to false (fire-and-forget publish)
tspstr / string, optionalISO-8601 timestamp override. Defaults to the current time

In Python the options are keyword arguments (report_error(error, level=..., append=..., tsp=...)); in JavaScript they are passed via an options object (reportError(error, { level, append, tsp })).

publish

Publishes a message to any WAMP topic. Use this for custom messaging or events that don’t map to a database table.

await ironflock.publish("com.myapp.alerts", { "level": "warning", "message": "Temperature threshold exceeded" })

Querying Historical Data

getHistory

Retrieves historical data from a fleet table. Supports filtering, time ranges, and pagination.

# Simple query data = await ironflock.getHistory("sensordata", {"limit": 100}) # Query with time range and filters data = await ironflock.getHistory("sensordata", { "limit": 500, "offset": 0, "timeRange": { "start": "2026-01-01T00:00:00Z", "end": "2026-03-01T00:00:00Z" }, "filterAnd": [ {"column": "temperature", "operator": ">", "value": 20}, {"column": "humidity", "operator": "<=", "value": 80} ] }) # Current value(s) only: the "latest" marker returns the newest row per entity current = await ironflock.getHistory("sensordata", { "limit": 100, "filterAnd": [{"latest": True}] })

Query parameters:

FieldTypeDescription
limitint / numberMaximum rows to return (1–10,000, required)
offsetint / numberOffset for pagination
timeRangedict / object{"start": "<ISO datetime>", "end": "<ISO datetime>"}
filterAndlist / arrayAND filter conditions, and/or the latest marker (see below)
columnslist / arrayColumns to return (optional). tsp, device_key and authid are always included; omit for all columns

Filter operators: =, !=, >, <, >=, <=, LIKE, ILIKE, IN, NOT IN, IS, IS NOT

Each filter is an object with column, operator, and value keys.

Reading current values. A {"latest": true} entry in filterAnd is not a filter condition but a mode switch: the data backend returns only the newest row per entity, derived in SQL from the entity key the table declares with maintainLatestFlagFor. A table without an entity key returns its single most recent row.

Other conditions combine with the marker as you would expect: conditions on entity-key columns narrow down which entities are returned, while all other conditions and timeRange are applied to the resulting latest rows. So combining {"latest": true} with a deleted filter hides deleted entities instead of resurfacing their previous row.

Earlier versions of IronFlock stored a physical latest_flag column. It no longer exists — a legacy latest_flag = true filter is still accepted and treated as the marker, but new code should use {"latest": true}. The latest marker is not available in getSeriesHistory.

getSeriesHistory / get_series_history

Retrieves down-sampled time-series data from a fleet table: numeric columns aggregated into time buckets (e.g. hourly averages). Ideal for charts spanning long time ranges. Available for tables (not transforms).

series = await ironflock.get_series_history("sensordata", { "metrics": ["temperature", "humidity"], "method": "AVG", "limit": 500, "timeRange": ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"], "groupBy": ["device_id"] })

Query parameters:

FieldTypeDescription
metricslist / arrayNumeric columns to down-sample
methodstr / stringAggregation per bucket: "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" or "LAST"
limitint / numberMaximum number of buckets (1–10,000)
timeRangelist / array[start, end] — ISO datetime strings or epoch-ms numbers; null = open end (required)
groupBylist / arrayColumns to group the series by (optional)
filterAndlist / arrayAND filter conditions (optional). Filter conditions only — the latest marker is not supported here; use getHistory to read current values

Subscribing to Data

subscribeToTable / subscribe_to_table

Subscribes to real-time updates on a fleet table. The handler is called whenever new data is published to the table. Rows written via the bulk insert path (publishRowsToTable / appendRowsToTable) are delivered to your handler one at a time, so handler code stays the same regardless of how the data was written.

def on_sensor_data(*args, **kwargs): print("New reading:", args, kwargs) await ironflock.subscribe_to_table("sensordata", on_sensor_data)

subscribe

Subscribes to any WAMP topic for custom real-time messaging.

def on_alert(*args, **kwargs): print("Alert received:", args, kwargs) await ironflock.subscribe("com.myapp.alerts", on_alert)

Cross-App Data Access

Read another app’s fleet data from within your app, in the same project. The provider app must list your app in its data-template.yml consumes: section, and the project user must grant access. Access is read-only: you can query history and subscribe to real-time rows of the tables and transforms the provider shares, but you cannot write to them. Consumed-app connections are cached per app and closed automatically when your instance stops.

If your app holds the wildcard grant (consumes: [{ app: "*" }]), you can discover and open providers dynamically with listConsumableApps / list_consumable_apps and connectToAllApps / connect_to_all_apps (below).

connectToApp / connect_to_app

Opens a read-only connection to another app’s data backend and returns a handle. The handle exposes getHistory / get_history, subscribeToTable / subscribe_to_table and getSeriesHistory / get_series_history (tables only) — the same querying and subscribing you use on your own tables — plus close and the shared tables / transforms catalogs.

# Open a read-only handle on another app's data backend weather = await ironflock.connect_to_app("weather-app") # Inspect what the provider shares print([t["tablename"] for t in weather.tables]) # Query history and subscribe, just like your own tables rows = await weather.get_history("forecasts", {"limit": 100}) def on_forecast(*args, **kwargs): print("New forecast:", args) await weather.subscribe_to_table("forecasts", on_forecast)

Parameters:

ParameterTypeDescription
app_name / appNamestr / stringProvider app name, as declared in your consumes: section
stagestr / string, optionalProvider stage: "dev" or "prod". Defaults to your app’s own stage
on_error / onErrorcallable, optionalCalled with a CrossAppAccessError if access is denied after the connection was established (e.g. the grant is later revoked)

If access is denied or misused, a CrossAppAccessError is raised (Python) / thrown (JavaScript) with a code field: NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, or NOT_AUTHORIZED.

In Python, stage and on_error are keyword arguments; in JavaScript they are passed via an options object (connectToApp(appName, { stage, onError })).

listConsumableApps / list_consumable_apps

Lists every non-private provider in the project — the discovery primitive for apps that hold the wildcard consume grant (consumes: [{ app: "*" }] in your data-template.yml, granted by the project user). It performs a single call and opens no connections: render the returned catalogs in a picker, then call connectToApp / connect_to_app for the ones you want — or connectToAllApps / connect_to_all_apps to open them all at once.

Note: Declare the grant in your app’s data-template.yml, and quote the * — a bare * is a YAML alias and won’t parse:

consumes: - app: "*"
providers = await ironflock.list_consumable_apps() for p in providers: print(p["app"], list(p["stages"].keys())) # e.g. "weather-app" ["dev", "prod"]

Each entry describes one provider:

FieldTypeDescription
appstr / stringProvider app name
provider_app_keyint / numberThe provider’s app key
stagesdict / objectPer-stage catalog { dev?, prod? }; a stage is present only if the provider has a data backend for it. Each catalog holds the non-private tables and transforms it shares

Raises (Python) / throws (JavaScript) a CrossAppAccessError with code: NO_GRANT if your app holds no wildcard grant.

connectToAllApps / connect_to_all_apps

Opens read-only handles to every non-private provider in the project in one call (wildcard consumers only). Enumerates providers via listConsumableApps / list_consumable_apps and opens each one, skipping any without a data backend for the requested stage. Each handle is cached under the same key as connectToApp / connect_to_app, so a later connectToApp(name) returns the already-warmed handle. The returned handles are closed together when your instance stops.

apps = await ironflock.connect_to_all_apps( on_error=lambda err: print("Provider skipped:", err) ) for app in apps: rows = await app.get_history(app.tables[0]["tablename"], {"limit": 10}) print(app.app, rows)

Parameters:

ParameterTypeDescription
stagestr / string, optionalProvider stage: "dev" or "prod". Defaults to your app’s own stage
continue_on_error / continueOnErrorbool / boolean, optionalWhen true (the default), a provider that fails to open is reported to on_error / onError and omitted from the result. When false, the first failure is raised/thrown
on_error / onErrorcallable, optionalCalled with each provider that could not be opened (while continue_on_error / continueOnError is true), and with a CrossAppAccessError if an already-opened connection is later denied (e.g. the grant is revoked)

Returns the successfully-opened provider handles (same handle type as connectToApp / connect_to_app). Raises (Python) / throws (JavaScript) a CrossAppAccessError with code: NO_GRANT if your app holds no wildcard grant.

In Python, stage, on_error and continue_on_error are keyword arguments; in JavaScript they are passed via an options object (connectToAllApps({ stage, onError, continueOnError })).

Managed File Storage

Every app data backend gets private object storage alongside its tables, reached through the files property. Use it for images, PDFs, camera frames, firmware blobs — anything that does not belong in a table row. No setup is required: an app with no files: section in its data template still gets one namespace called default.

The key idea is that storing an object hands you back a permanent URL you can write straight into a table column, so a dashboard widget can render it with no further work:

# Store an object and get a permanent URL back in the same call info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg") # The URL is safe to store in a table column — a dashboard widget can then # render <img src="{{photo_url}}"> without any extra round trip await ironflock.publish_to_table("inspections", part_id="1", photo_url=info.url) # Read it back data = await ironflock.files.get("part-1.jpg") # Walk every object under a prefix (pages are fetched for you) async for obj in ironflock.files.iter(prefix="2026/"): print(obj.key, obj.size)

That URL never expires, but it is not a public link: it stays readable only to an authenticated requestor holding READ access on this data backend, and an auth proxy re-checks that on every request. It is therefore safe to store in the database.

Namespaces

A namespace is a key prefix that carries policy — retention, sharing rules, allowed content types. It is not a separate bucket; every namespace of an app lives inside that app’s single storage area. Declare one only when a set of objects needs different rules; otherwise stay in default and organise your objects with key paths like 2026/03/part-1.jpg.

Declare extra namespaces in data-template.yml:

files: # Storage budget the app suggests for itself. The project user can change it, # and their setting is the one that gets enforced. quotaBytes: 5368709120 namespaces: - name: frames description: Raw camera frames, one JPEG per inspected part. contentTypes: ["image/jpeg"] maxObjectBytes: 20971520 retention: { deleteAfter: 30 days }

Note that the budget is declared once for the whole app, not per namespace. A namespace is only a key prefix inside the app’s single storage area, so there is nothing for a per-prefix budget to be enforced against. maxObjectBytes is per namespace — it caps a single object, not a total.

Every method below takes the namespace as an optional argument and defaults to default.

Storing and reading objects

# Bytes in, bytes out info = await ironflock.files.put("reports/march.pdf", pdf_bytes, content_type="application/pdf") data = await ironflock.files.get("reports/march.pdf") # Or straight from/to a local file — these stream on the large-object path, # so a multi-gigabyte file never has to fit in memory await ironflock.files.put_file("firmware/v2.bin", "/data/build/v2.bin") await ironflock.files.get_to_file("firmware/v2.bin", "/tmp/v2.bin")
MethodDescription
put(key, data, …)Stores an object (bytes in Python, Uint8Array in JavaScript). Returns the object’s metadata including its url
get(key, namespace?)Returns the object’s content
put_file(key, path, …) / get_to_file(key, path, …)Python only. Store from, or write to, a local file. Streams on the large-object path
delete(key, namespace?)Deletes an object
copy(key, to, …)Copies an object, optionally into another namespace
move(key, to, …)Copy-then-delete. Not atomic — the service has no move verb, so a failed delete leaves both copies

JavaScript has no file-path helpers because the package ships a single build for both Node and the browser — read and write local files yourself with fs.

put accepts: content_type / contentType (the MIME type; the namespace may restrict which are allowed) and namespace. In Python these are keyword arguments; in JavaScript they go in an options object.

Listing and inspecting

MethodDescription
list(…)One page of objects. Returns objects, prefixes, is_truncated / isTruncated and a cursor to pass back for the next page
iter(…) / iterate(…)Async iterator over every object under a prefix, paginating automatically. Named iter in Python, iterate in JavaScript
stat(key, namespace?)Metadata for one object without transferring its content
exists(key, namespace?)Whether an object exists
namespaces()The namespaces this app may use
usage(…)How much storage the app is using — see below
catalog()Namespaces plus server-issued limits and quotas. Cached after the first call

Objects are described by the same fields in both SDKs, in each language’s naming style: namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 and url.

Storage usage and quota

usage answers from the object store in a single call, so the totals are exact rather than added up by the SDK:

u = await ironflock.files.usage() print(u.size_bytes, u.object_count, u.quota_bytes, u.free_bytes) # Break the total down per namespace (costs one listing per namespace) detailed = await ironflock.files.usage(detail=True) print(detailed.per_namespace) # {"default": 1048576, "frames": 73400320}
FieldMeaning
size_bytes / sizeBytesBytes currently stored
object_count / objectCountNumber of objects stored
quota_bytes / quotaBytesThe enforced budget. 0 means unlimited
free_bytes / freeBytesBytes remaining. -1 means unlimited — reporting 0 there would read as “full”
per_namespace / perNamespaceBytes per namespace. Present only when you ask for the detailed breakdown

The per-namespace breakdown is off by default because the store cannot answer it directly: it accounts per storage area, and a namespace is only a prefix, so the SDK has to list each namespace to add the sizes up. Ask for it when you want it, not on a hot path.

Two different quotas show up, and it is worth keeping them apart. catalog() reports both:

FieldMeaning
quota_bytes / quotaBytesWhat is actually enforced, read from the object store — the project user’s setting
suggested_quota_bytes / suggestedQuotaBytesWhat the app’s data template asked for. 0 if it asked for nothing

They differ whenever a user has raised or lowered the app’s budget, which is why the enforced value is read from the store rather than from the template — redeploying the app must not silently reset a user’s choice. A UI can show both (“the app suggests X, you have set Y”). Enforcement always uses the first one.

Sharing objects

There are two kinds of link, and the difference matters:

MethodLifetimeWho can read it
url(key, …)PermanentOnly an authenticated requestor with READ on this data backend — re-checked on every request. Safe to store in a table column
share_url / shareUrlExpiring (default 15 min, server-clamped)Anyone holding the link. Nothing re-checks authorization when it is used

share_url / shareUrl is a bearer capability: hand it to a person who needs temporary access, and do not store it in the database. Use url for anything a dashboard renders.

url returns None / undefined where the deployment has no HTTP edge (for example a plain-HTTP appliance) — that is the signal to fall back to get. Passing an object’s etag as the version argument lets browsers cache the response immutably.

upload_url / uploadUrl mints an expiring URL that accepts a direct upload, returning url, method, headers and expires_in / expiresIn. Send exactly the headers it returns, or the signature will not verify.

Large objects

The SDK chooses the transport by size, automatically — there is nothing to configure:

Object sizeHow it travels
Up to the inline limit (currently 6 MiB)A single call through the message router
LargerDirectly to object storage over HTTPS, bypassing the router

The exact limit is reported by the server at runtime as inline_max_bytes / inlineMaxBytes in catalog(), so it can be raised without an SDK release.

Two ceilings remain, and both report TOO_LARGE with a reason naming which one you hit:

  • 5 GiB — the single-upload limit of the object store. Multipart upload is not implemented yet.
  • The inline limit, where there is no direct endpoint — an air-gapped appliance cannot transfer a large object at all. No retry or smaller chunk will help, and the message says so.

The direct path needs the device to reach the object storage host, not just the router. Two failures common in the field get their own codes instead of looking like authorization problems: PRESIGN_UNREACHABLE (a proxy that allows only the router) and CLOCK_SKEW (the object store rejects requests more than 15 minutes out of step — check NTP on the device).

File storage errors

Every file operation raises (Python) / throws (JavaScript) a FileStoreError carrying a stable code and a human-readable reason. Branch on code, never on reason.

from ironflock.filestore import FileStoreError try: await ironflock.files.put("huge.bin", payload) except FileStoreError as e: if e.code == "QUOTA_EXCEEDED": print("Filestore is full:", e.reason) else: raise
CodeMeaning
NOT_AUTHORIZEDThe caller may not perform this operation
NO_SUCH_NAMESPACEThe namespace is not declared in the data template
NO_SUCH_OBJECTThe key does not exist
TOO_LARGEExceeds the single-call transfer limit
OBJECT_TOO_LARGEExceeds the namespace’s own maxObjectBytes
QUOTA_EXCEEDEDThe filestore is full
CONTENT_TYPE_NOT_ALLOWEDThe namespace restricts contentTypes
NOT_SUPPORTEDThe backend cannot do this
NOT_AVAILABLEThis deployment has no file service
PRESIGN_UNREACHABLEObject storage is not reachable directly (a proxy?)
CLOCK_SKEWThe device clock is too far out of step
INTERNALAnything else

A newer server may introduce codes this SDK release does not know. They are passed through as code rather than collapsed, so treat an unrecognised value as a generic failure.

In Python, FileStoreError is imported from ironflock.filestore; in JavaScript it is exported from the package root (import { FileStoreError } from "ironflock").

Cross-Device Communication

registerDeviceFunction / register_device_function

Registers a procedure that other devices in the same project can call. The SDK automatically namespaces the procedure to the current device.

def add(a, b): return a + b await ironflock.register_device_function("com.myapp.add", add)

register() is an alias for register_device_function().

callDeviceFunction / call_device_function

Calls a procedure registered by another device. The SDK assembles the full WAMP topic automatically using the target device’s key.

result = await ironflock.call_device_function( 42, # target device key "com.myapp.add", # procedure name args=[3, 5] # arguments ) print(result) # 8

call

Calls a remote procedure using a full WAMP URI. Use this for direct calls when you know the exact topic.

result = await ironflock.call("some.full.wamp.topic", args=[42])

Device Metadata

setDeviceLocation / set_device_location

Updates the device’s GPS location in the platform. Changes are reflected in real time on IronFlock maps.

await ironflock.set_device_location(long=8.6821, lat=50.1109)
ParameterRange
long-180 to 180
lat-90 to 90

Location history is not stored. To track location over time, create a dedicated table and use publish_to_table / publishToTable.

getRemoteAccessUrlForPort

Returns the public remote access URL for a given port on the device.

url = ironflock.getRemoteAccessUrlForPort(8080) # "https://<device_key>-<app_name>-8080.app.ironflock.com"

Connection Properties & Lifecycle

PropertyTypeDescription
is_connectedboolWhether the platform connection is active
connectionCrossbarConnectionThe underlying connection instance (advanced use)
MethodDescription
run()Starts the connection and runs mainFunc (blocking)
await start()Starts the connection asynchronously
await stop()Stops the connection and cancels running tasks
await run_async()Starts and keeps the connection running asynchronously

Error Handling

Every SDK method fails loudly: on invalid arguments, a lost connection, or a rejection from the platform, it raises an exception (Python) or rejects (JavaScript) with a message naming the operation, the topic and the reason. Nothing is silently swallowed, so wrap calls you want to survive in a try block.

try: rows = await ironflock.getHistory("sensordata", {"limit": 100}) except ValueError as e: # Invalid parameters — e.g. limit out of range, or a malformed filter print(f"Bad query: {e}") except RuntimeError as e: # Not connected, table not in the data-template, or the platform rejected the call print(f"Query failed: {e}")

In JavaScript, failures coming from the platform are WampError instances — a normal Error subclass that additionally carries the WAMP error URI in error and the error payload in args / kwargs. Everything else (bad parameters, no connection) is a plain Error.

Upgrading: older SDK versions logged a message and returned None / null when a call failed. They now raise instead, so code shaped like if result is None: no longer detects failures — use try / except (or try / catch).

Browser Usage (JavaScript only)

The JavaScript SDK works in modern browsers. Since browsers don’t have environment variables, pass all config via the constructor:

import { IronFlock } from "ironflock"; const ironflock = new IronFlock({ serialNumber: "device-serial-from-server", deviceKey: "my-device-key", appName: "MyWebApp", swarmKey: 10, appKey: 20, env: "PROD", }); await ironflock.start(); await ironflock.publishToTable("sensordata", [{ temperature: 22 }]);

Use IronFlock.fromServer() to fetch configuration from your backend instead of hardcoding credentials:

const ironflock = await IronFlock.fromServer("/api/ironflock-config"); await ironflock.start();

Your backend endpoint should return a JSON object with the connection options (serialNumber, deviceKey, appName, swarmKey, appKey, env).

Registering AI Agent Functions

The SDK can register functions that are callable by AI agents. Register a procedure and reference its topic in your ai-template.yml:

def get_sensor_reading(sensor_id): """Returns the latest reading from a sensor.""" reading = read_from_hardware(sensor_id) return { "sensor_id": sensor_id, "temperature": reading.temp, "humidity": reading.hum, "timestamp": reading.ts } await ironflock.register_device_function("sensors.get_latest", get_sensor_reading)

The AI agent can then call this function when a user asks a question that requires live sensor data.

To wire the registered WAMP topic to an AI agent, reference it in your app’s .ironflock/ai-template.yml:

sensor_agent: tool_description: | Delegate to this agent when the user asks about sensor readings, live device data, or current environmental conditions. system_prompt: | You are a sensor data specialist. Use get_current to retrieve the latest reading from any sensor. Always include the unit in your response. main: true max_context_tokens: 30000 max_iterations: 5 tools: get_current: description: Returns the latest reading from a sensor. topic: sensors.get_latest parameters: sensor_id: type: string description: The sensor identifier to query. required: true

The topic value (sensors.get_latest) must match the name passed to register_device_function / registerDeviceFunction in your edge code. IronFlock automatically routes the call to the device where the function is registered.

For the full ai-template.yml reference, see Defining Agents & Tools.

Environment Variables

These variables are set automatically by the IronFlock runtime inside app containers:

VariableDescription
DEVICE_NAMEDevice display name
DEVICE_SERIAL_NUMBERUnique, immutable device identifier
DEVICE_KEYDevice key for authentication
SWARM_KEYProject identifier
APP_KEYApp identifier
APP_NAMEApp name
ENVEnvironment: DEV or PROD
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")
Last updated on