@microclimates/envos-app
v0.1.1
Published
Build Node.js apps for EnvOS: a typed client, live card updates, triggers, test helpers, and a scaffolder made for AI-assisted coding.
Maintainers
Readme
envos-app
Build Node.js apps for EnvOS, the Microclimates environmental control platform.
Read live sensor data, react to changes as they happen, run tasks and drive controls, from any Node.js program. envos-app gives you a typed client, live card updates, triggers, safety rails for writes, a mock site for tests, and a project template that AI coding tools understand.
import { defineApp, onThreshold } from "@microclimates/envos-app";
export default defineApp({
name: "frost-watch",
triggers: [
onThreshold({ category: "monitor", field: "state.out.temp", below: 38, hysteresis: 2 }, ({ card, state, value }, ctx) => {
ctx.log.warn(`${card.config.title}: frost risk ${state} at ${value}°F`);
}),
],
});Contents
- Quick start
- How EnvOS is organized
- Connecting to sites
- Reading data
- Live data
- Apps and triggers
- Writing safely
- Testing
- Building with AI coding tools
- CLI
- Developer portal
- Requirements
Quick start
You need Node.js 22.18 or later, your site's URL (e.g. https://c1234-abcd.microclimates.com),
and an API token for it.
npx @microclimates/envos-app create my-app
cd my-app
npm install
cp .env.example .env # paste your token
npm run doctor # checks URL, token and live connection
npm run dev # runs the starter appThe starter app watches every monitor on the site and logs health changes and high humidity. It
only reads, so a view token is enough.
Using envos-app in an existing project
npm install @microclimates/envos-app
npx envos-app add-site main --url https://c1234-abcd.microclimates.com --auth view
echo "ENVOS_MAIN_TOKEN=<token>" >> .envimport { EnvosClient } from "@microclimates/envos-app";
const client = await EnvosClient.fromConfig();
const site = client.site("main");
for (const card of await site.cards.list({ filter: 'component.category="monitor"' })) {
console.log(card.config.title, card.state.out.health);
}
await client.close();For a one-off script you can skip envos.json: set ENVOS_URL, ENVOS_TOKEN and optionally
ENVOS_AUTH, and EnvosClient.fromConfig() uses a single site named default.
How EnvOS is organized
Everything on a site is a card: a sensor, a relay, a climate monitor, an irrigation program.
| Part | Meaning |
|---|---|
| card.component | The class: id, category, field definitions with labels and units, tasks. Read-only. |
| card.config | How this card is set up: title, zone, tags, class-specific settings, outbound wires. |
| card.state.in | What you ask the card to do. |
| card.state.out | What the card reports. Only the card writes it. |
Wires carry one card's state.out value into another card's state.in. The
component.category tells you where a card sits on the signal path:
device → monitor → automation → control → device
(how) (what) (decide) (what) (how)Apps that act behave like automations: they read monitors and drive controls, and EnvOS stages the hardware. The full model is in agents/envos-concepts.md.
Connecting to sites
Sites live in envos.json, which holds no secrets:
{
"$schema": "./node_modules/@microclimates/envos-app/schema/envos.schema.json",
"sites": {
"north": { "url": "https://c1234-abcd.microclimates.com", "auth": "control", "description": "North greenhouse" },
"lab": { "url": "https://192.168.10.20:8443", "auth": "view", "dryRun": true }
}
}| Key | Meaning |
|---|---|
| url | Site URL. The developer portal's …/api/v2 form works too. |
| auth | The level the site's token was issued with: view, control, config or admin. envos-app refuses writes above it before sending them. |
| tokenEnv | Variable holding the token. Default ENVOS_<NAME>_TOKEN. |
| dryRun | Log and audit writes without sending them. |
| deviceWrites | warn (default), deny or allow inputs and tasks sent straight to device cards. |
| description | For people and coding agents. |
Tokens come from the environment or .env. Each token level includes the ones before it:
| Level | Can |
|---|---|
| view | Read cards, schemas, history, events, wires |
| control | Also send inputs and run tasks |
| config | Also change card configuration |
| admin | Everything. Apps should not need it. |
const client = await EnvosClient.fromConfig();
client.site("north"); // one site
client.sites(); // all of themReading data
Each site groups the REST API by area. Every method is typed and documented in your editor.
const site = client.site("north");
await site.health(); // { envosVersion }
await site.cards.list({ filter: 'state.out.health!="ok"', include: ["id", "config.title"] });
await site.cards.get(id); // complete card
await site.cards.getMany([id1, id2]);
await site.schemas.get(id); // JSON Schema: fields, units, ranges
await site.history.metrics(id); // what the card keeps
await site.history.series(id, { period: "last_24h", metrics: ["temp"], interval: "15m" });
await site.history.summary(id, { period: "last_7d" }); // min/max/avg/trend + narrative
await site.history.seriesCsv(id, { period: "last_30d" });
await site.events.list(id, { period: "last_24h", level: "error" });
await site.wires.list({ card: id }); // edges in and out, resolvedErrors are typed and carry a stable code (ENVOS_AUTH, ENVOS_NOT_FOUND, ENVOS_BAD_REQUEST,
ENVOS_NETWORK …). Reads retry on network errors and 502/503/504. See
agents/troubleshooting.md.
Live data
EnvOS pushes every card change as it happens. Connect once and envos-app keeps every card in memory:
await site.connect();
site.live.get(id); // instant, no request
site.live.list({ category: "monitor", zone: "Bay 1" });
const stop = site.onCardChange((card, prior, { changed }) => {
console.log(card.config.title, changed, prior?.state.out.humid, "→", card.state.out.humid);
}, { category: "monitor", fields: ["state.out.humid"] });- Handlers receive their own copies of cards; the shared class definitions cannot be altered.
- Dropped connections reconnect automatically, and changes missed in between are delivered on
reconnect. Watch
site.onConnectionChange()to pause decisions while reconnecting. - Select cards with
cardId,category,componentId,zone,tagor awherefunction.
Apps and triggers
defineApp describes an app; runApp loads envos.json, connects the sites the triggers use, runs
setup, starts the triggers, and stops cleanly on Ctrl+C.
// src/app.ts
import { defineApp, onCardChange, onHealthChange, onThreshold } from "@microclimates/envos-app";
export default defineApp({
name: "bay-watch",
setup(ctx) {
ctx.log.info(`${ctx.site("north").live.size} cards loaded`);
},
triggers: [
onHealthChange({ site: "north", category: "monitor" }, ({ card, priorHealth, health }, ctx) => {
ctx.log.warn(`${card.config.title}: ${priorHealth} → ${health}`);
}),
onThreshold({ site: "north", cardId: bay1, field: "state.out.humid", above: 85, hysteresis: 3 }, (event, ctx) => {
ctx.log.warn(`humidity ${event.state}`, { value: event.value });
}),
onCardChange({ site: "north", category: "control", fields: ["state.out.value"] }, (card, prior, ctx) => {
ctx.log.info(`${card.config.title} now at ${card.state.out.value}%`);
}),
],
});// src/main.ts
import { runApp } from "@microclimates/envos-app";
import app from "./app.ts";
await runApp(app);| Trigger | Fires |
|---|---|
| onCardChange(selector, (card, prior, ctx) => …) | On every change to a matching card, optionally only when fields change |
| onThreshold({ field, above?, below?, hysteresis? }, (event, ctx) => …) | Once when a value crosses a limit and once when it returns past the hysteresis band; also at startup if already past |
| onHealthChange(selector, (event, ctx) => …) | When state.out.health moves between ok, warn and alert |
Handler errors are logged, never thrown into the stream.
Writing safely
EnvOS moves real equipment, so every write goes through the same checks:
await site.cards.setInput(foggerId, { value: 40 }); // control: ask a card to do something
await site.cards.queueTask(valveId, "Irrigate Plants"); // control: start a task with defaults
await site.cards.runTask(valveId, { key: "irrigate", name: "Irrigate Plants", inputs: { mins: 20 } });
await site.cards.stopTask(valveId, "Irrigate Plants");
await site.cards.setRunning(cardId, false); // take out of service
await site.cards.setConfig(cardId, { notes: "Replaced sensor" }); // config- Declared auth level. A write above the site's
auththrowsWriteDeniedErrorwithout sending anything. - Device policy. Inputs sent straight to device cards log a warning (or are refused with
deviceWrites: "deny"). Drive control cards instead. - Dry run. With
dryRun: true, writes are logged and audited, not sent. - Audit. Every attempt (sent, dry run, denied, failed) is recorded. Add
jsonlAuditSink("audit/writes.jsonl")for a file. - Requests, not results. Writes resolve with
queued: true; the card decides what happens. Readstate.outto confirm.
Writes are never retried. See agents/safety-rules.md.
Testing
@microclimates/envos-app/testing runs a fake EnvOS site on localhost with the same REST API, errors, auth
behavior and live stream, so apps can be tested without hardware or tokens.
import { EnvosClient, runApp } from "@microclimates/envos-app";
import { SAMPLE_IDS, sampleSite, startMockEnvos } from "@microclimates/envos-app/testing";
import app from "../src/app.ts";
test("boosts the fogger when humidity drops", async () => {
const mock = await startMockEnvos({ cards: sampleSite(), auth: "control" });
const client = new EnvosClient({ sites: { north: { url: mock.url, token: mock.token, auth: "control" } } });
const running = await runApp(app, { client, handleSignals: false });
mock.updateCard(SAMPLE_IDS.climate, { out: { humid: 48 } });
await expect.poll(() => mock.writes).toContainEqual(expect.objectContaining({ cardId: SAMPLE_IDS.fogger }));
await running.stop();
await client.close();
await mock.close();
});sampleSite(): a small greenhouse along the full signal path (weather station, climate monitor, recipe, fogger control, relay board).cardFixture({ id, category, out, … }): your own cards with your real field names.mock.updateCard(),addCard(),removeCard(),dropConnections(): drive the site.mock.writes,mock.requests: assert on what your app did.waitForCard(site, id, predicate): wait for live data to reflect something.
Building with AI coding tools
Every project made by envos-app create includes:
AGENTS.md, read by Claude Code, Cursor, Copilot, Codex and others. It covers the commands, the EnvOS model, the API, and the rules. It also has a generated table of the project's sites with each token's auth level, so the agent knows what it may change.- Skills in
.claude/skills/: inspect a site, add a trigger, add a site. scripts/inspect-card.ts, which prints a card's real fields, units, tasks, metrics and wires, so agents look up names instead of guessing them.- Guides shipped in the package under
node_modules/@microclimates/envos-app/agents/, always matching the installed version: concepts, cookbook, safety rules, troubleshooting.
Then ask for what you want: "Alert me by email when any bay's temperature stays above 90°F for 10 minutes", "Add our south greenhouse as a second site", "Show a daily summary of humidity per zone." The agent writes the trigger, the test, and tells you what it will write and where.
CLI
| Command | Does |
|---|---|
| npx @microclimates/envos-app create [dir] | New project from a template (--url, --auth, --site, --template, --yes) |
| npx envos-app add-site <name> --url <url> --auth <level> | Add or update a site in envos.json, .env.example and AGENTS.md |
| npx envos-app doctor [--site name] | Check URL, token variable, reachability, token, live stream |
| npx envos-app cards [--site name] [--filter expr] [--json] | List the cards on a site |
doctor and cards read .env from the project.
Developer portal
The EnvOS developer portal lets you explore a site's API
interactively. envos-app covers the same operations one to one. @microclimates/envos-app/portal exports the
operation list and generates envos-app code for any request made in the portal:
import { envosAppSnippet } from "@microclimates/envos-app/portal";
envosAppSnippet({ method: "GET", path: "/cards/:id/history", params: { id }, query: { period: "last_24h" } });A test checks this list against the EnvOS OpenAPI contract, so the package, the portal and EnvOS cannot drift apart unnoticed. See docs/developer-portal.md.
Requirements
- Node.js 22.18 or later.
- An EnvOS site with REST API v2, and an API token for it.
More
License
MIT © Microclimates, Inc.
