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.
| SDK | Package | Requires |
|---|---|---|
| Python | ironflock on PyPI | Python 3.8+ |
| JavaScript | ironflock on npm | Node.js 18+ or modern browser |
Installation
Python
pip install ironflockOr add ironflock to your app’s requirements.txt.
Quick Start
Python
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
Python
ironflock = IronFlock(
mainFunc=main, # async function to run after connecting
serial_number="abc123" # override device serial (optional)
)| Parameter | Description |
|---|---|
mainFunc | An async function that runs once the connection is established |
serial_number | Override 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.
Python
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.
Python
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.
Python
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.
Python
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.
Python
# 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:
| Parameter | Type | Description |
|---|---|---|
error | str / string or exception / Error | The error message, or an exception whose traceback/stack (or message) is recorded |
level | str / string, optional | Severity: "error", "warn", "info" or "debug". Defaults to "error" |
append | bool / boolean, optional | When true, use the append RPC (returns the insert outcome). Defaults to false (fire-and-forget publish) |
tsp | str / string, optional | ISO-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.
Python
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.
Python
# 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:
| Field | Type | Description |
|---|---|---|
limit | int / number | Maximum rows to return (1–10,000, required) |
offset | int / number | Offset for pagination |
timeRange | dict / object | {"start": "<ISO datetime>", "end": "<ISO datetime>"} |
filterAnd | list / array | AND filter conditions, and/or the latest marker (see below) |
columns | list / array | Columns 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).
Python
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:
| Field | Type | Description |
|---|---|---|
metrics | list / array | Numeric columns to down-sample |
method | str / string | Aggregation per bucket: "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" or "LAST" |
limit | int / number | Maximum number of buckets (1–10,000) |
timeRange | list / array | [start, end] — ISO datetime strings or epoch-ms numbers; null = open end (required) |
groupBy | list / array | Columns to group the series by (optional) |
filterAnd | list / array | AND 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.
Python
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.
Python
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.
Python
# 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:
| Parameter | Type | Description |
|---|---|---|
app_name / appName | str / string | Provider app name, as declared in your consumes: section |
stage | str / string, optional | Provider stage: "dev" or "prod". Defaults to your app’s own stage |
on_error / onError | callable, optional | Called 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,
stageandon_errorare 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: "*"Python
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:
| Field | Type | Description |
|---|---|---|
app | str / string | Provider app name |
provider_app_key | int / number | The provider’s app key |
stages | dict / object | Per-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.
Python
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:
| Parameter | Type | Description |
|---|---|---|
stage | str / string, optional | Provider stage: "dev" or "prod". Defaults to your app’s own stage |
continue_on_error / continueOnError | bool / boolean, optional | When 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 / onError | callable, optional | Called 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_errorandcontinue_on_errorare 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:
Python
# 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
Python
# 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")| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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:
Python
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}| Field | Meaning |
|---|---|
size_bytes / sizeBytes | Bytes currently stored |
object_count / objectCount | Number of objects stored |
quota_bytes / quotaBytes | The enforced budget. 0 means unlimited |
free_bytes / freeBytes | Bytes remaining. -1 means unlimited — reporting 0 there would read as “full” |
per_namespace / perNamespace | Bytes 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:
| Field | Meaning |
|---|---|
quota_bytes / quotaBytes | What is actually enforced, read from the object store — the project user’s setting |
suggested_quota_bytes / suggestedQuotaBytes | What 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:
| Method | Lifetime | Who can read it |
|---|---|---|
url(key, …) | Permanent | Only an authenticated requestor with READ on this data backend — re-checked on every request. Safe to store in a table column |
share_url / shareUrl | Expiring (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 size | How it travels |
|---|---|
| Up to the inline limit (currently 6 MiB) | A single call through the message router |
| Larger | Directly 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.
Python
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| Code | Meaning |
|---|---|
NOT_AUTHORIZED | The caller may not perform this operation |
NO_SUCH_NAMESPACE | The namespace is not declared in the data template |
NO_SUCH_OBJECT | The key does not exist |
TOO_LARGE | Exceeds the single-call transfer limit |
OBJECT_TOO_LARGE | Exceeds the namespace’s own maxObjectBytes |
QUOTA_EXCEEDED | The filestore is full |
CONTENT_TYPE_NOT_ALLOWED | The namespace restricts contentTypes |
NOT_SUPPORTED | The backend cannot do this |
NOT_AVAILABLE | This deployment has no file service |
PRESIGN_UNREACHABLE | Object storage is not reachable directly (a proxy?) |
CLOCK_SKEW | The device clock is too far out of step |
INTERNAL | Anything 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,
FileStoreErroris imported fromironflock.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.
Python
def add(a, b):
return a + b
await ironflock.register_device_function("com.myapp.add", add)
register()is an alias forregister_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.
Python
result = await ironflock.call_device_function(
42, # target device key
"com.myapp.add", # procedure name
args=[3, 5] # arguments
)
print(result) # 8call
Calls a remote procedure using a full WAMP URI. Use this for direct calls when you know the exact topic.
Python
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.
Python
await ironflock.set_device_location(long=8.6821, lat=50.1109)| Parameter | Range |
|---|---|
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.
Python
url = ironflock.getRemoteAccessUrlForPort(8080)
# "https://<device_key>-<app_name>-8080.app.ironflock.com"Connection Properties & Lifecycle
Python
| Property | Type | Description |
|---|---|---|
is_connected | bool | Whether the platform connection is active |
connection | CrossbarConnection | The underlying connection instance (advanced use) |
| Method | Description |
|---|---|
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.
Python
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/nullwhen a call failed. They now raise instead, so code shaped likeif result is None:no longer detects failures — usetry/except(ortry/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:
Python
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: trueThe 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:
| Variable | Description |
|---|---|
DEVICE_NAME | Device display name |
DEVICE_SERIAL_NUMBER | Unique, immutable device identifier |
DEVICE_KEY | Device key for authentication |
SWARM_KEY | Project identifier |
APP_KEY | App identifier |
APP_NAME | App name |
ENV | Environment: DEV or PROD |
Python
import os
device_name = os.environ.get("DEVICE_NAME")
serial = os.environ.get("DEVICE_SERIAL_NUMBER")
project_key = os.environ.get("SWARM_KEY")