Skip to main content

Projects & Builds

A project is an AppBlocks application: a target device, a set of features (peripherals, protocols, logic) and the generated source code. A project version is an immutable snapshot of a project together with its compiled firmware — this is what gets deployed to devices over the air.

Base paths: /api/projects, /api/tasks, /api/workspace-projects.

MethodPathAuthPurpose
GET/api/projects/examplespublicList example projects
GET/api/projects/examples/categoriespublicExample categories
GET/api/projects/examples/:idpublicOne example with README
GET/api/projects/:id?sessionList projects, or get one
POST/api/projectssessionCreate a project
PUT/api/projects/:id?sessionUpdate a project
DELETE/api/projects/:idsessionDelete a project
DELETE/api/projectssessionDelete several projects
POST/api/projects/codepublicGenerate source code
POST/api/projects/downloadpublicDownload a project as a ZIP
POST/api/projects/savepublicAlias of /download
POST/api/projects/blockgenpublicGenerate blocks with AI
POST/api/projects/buildpublicBuild synchronously
GET/api/projects/:id/versions/:versionCode?sessionList or get project versions
POST/api/projects/:id/versionssessionCreate a version
PUT/api/projects/:id/versions/:versionCodesessionUpload binaries for a version
DELETE/api/projects/:id/versions/:versionCodesessionDelete a version
GET/api/projects/:id/versions/:versionCode/:filenamepublicDownload the application binary
GET/api/projects/:id/versions/:versionCode/firmware/:filenamepublicOTA firmware download (.tcu)
GET/api/projects/:id/versions/:versionCode/release/:filenamepublicRelease binary download
POST/api/tasksoptionalQueue a build job
GET/api/tasks/:idoptionalPoll a build job
DELETE/api/tasks/:idoptionalCancel a build job
POST/api/workspace-projects/exportsessionExport a workspace as a bundle
POST/api/workspace-projects/importsessionImport a bundle
POST/api/workspace-projects/updatesessionUpdate a workspace from a newer bundle

session in the Auth column means the request must be authenticated — an API key works just as well as a signed-in session. See Authentication.

Access rules

  • A project with no workspace belongs to its creator.
  • A workspace project is readable by anyone with workspace access.
  • A workspace project is writable by the workspace owner, admins, operators and members — but not by tenant-only users, who get 403 {"error":"Tenant users are not allowed to edit projects"}.
  • Ownership fields are set at creation time. A PUT cannot move a project between users or workspaces.

Projects

List projects

GET /api/projects
Query parameterDescription
startOffset, default 0
countPage size, default 10
searchCase-insensitive match on project name
workspaceReturn that workspace's projects instead of personal ones
<field>___<op>Field filters
{
"count": 12,
"data": [
{
"_id": "664a1f2b8c9d0e0011223301",
"name": "Boiler control",
"shortDescription": "Two-zone boiler controller",
"description": "…",
"device": { "platform": "…", "name": "…" },
"features": [ /* … */ ],
"runtime": "tios",
"version": "3",
"workspace": "664a1f2b8c9d0e0011223344",
"updatedAt": "…"
}
]
}

Internal node_* features are stripped from list results. Requesting a workspace you do not belong to returns 404 {"error":"Workspace Not Found"}.

Get one project

GET /api/projects/:id

Returns the same envelope with a single-element data array (and count: 0). Unknown ids return 404 {"error":"Project Not Found"}.

Create and update

POST /api/projects          # create
PUT /api/projects/:id # update
{
"name": "Boiler control",
"description": "Full description",
"shortDescription": "Two-zone boiler controller",
"device": { "platform": "…", "name": "…" },
"features": [ /* feature definitions */ ],
"featureFlags": { },
"featureFlagMap": { },
"version": "3",
"runtime": "tios",
"workspace": "664a1f2b8c9d0e0011223344"
}

workspace is honoured only when creating; the caller must belong to it. runtime is the target runtime (for example tios or zephyr). The full saved project is returned.

Delete projects

DELETE /api/projects/:id      # one
DELETE /api/projects # many
{ "projects": ["664a1f2b…", "664a1f2c…"] }

Bulk deletion is all-or-nothing: if the caller cannot edit every listed project, nothing is deleted and the response is 403 {"error":"Unauthorized"}.

Code generation and download

POST /api/projects/code
{
"params": {
"project": { /* project configuration */ },
"debug": true
}
}

Returns the generated files as an array of { name, contents }. debug defaults to true.

POST /api/projects/download
POST /api/projects/save

The body is the project configuration itself (not wrapped in params). The response is a ZIP (application/octet-stream, Content-Disposition: attachment; filename=<project name>.zip) containing the generated sources. For the tios runtime the required platform files are bundled in as well. Both paths run the same handler.

POST /api/projects/blockgen
{ "prompt": "blink an LED every second", "project": { /* … */ } }

Uses the AI block generator to produce blocks for the given project. Returns the generated blocks as JSON.

Builds

Building is normally asynchronous: submit a job, then poll it. Jobs are dispatched to build workers connected over Socket.IO and are deleted 3 minutes after creation, so poll promptly.

Queue a build

POST /api/tasks
{
"command": "build",
"files": [ { "name": "main.tbs", "contents": "…" } ],
"project": { /* project configuration, incl. runtime and device */ },
"debug": true,
"requireSymbols": false
}
{ "status": "running", "output": { "puuid": "66c1a0f2…" } }

puuid is the job id. requireSymbols: true keeps the symbol/ELF file in the result (needed for debugging and for OpenOCD upload).

caution

command must be "build". Any other value produces no response at all, so the request hangs until the client times out.

Poll a build

GET /api/tasks/:id
statusMeaningdata
runningStill queued or compiling{ output, progress }
completedSuccess{ output, binary, symbols, hex }
abortedCompilation failed, or required symbols were missing{ output }
unknownJob not found — invalid, cancelled or already expired{ output }

Binaries are returned as buffer payloads. For tios builds the compiler output is a .tpc/.pdb pair, which the response exposes as binary and symbols. Polling somebody else's job returns 403.

Server-side errors are reported as 200 with {"status":"error","data":{"output":"server error"}}.

Cancel a build

DELETE /api/tasks/:id

Deletes the job. Returns {"status":"aborted","data":{"output":"Compilation was aborted."}}. Cancelling another user's job returns 403.

Synchronous build

POST /api/projects/build

Same body as POST /api/tasks (without command). The request blocks until the build finishes and returns the artifacts directly:

{
"tpc": "<buffer>",
"pdb": "<buffer>",
"binary": "<buffer>",
"hex": "<buffer>",
"symbols": "<buffer>",
"output": "compiler output",
"project": { /* echoed back */ },
"status": "completed"
}

Failures return 500 { "output": "<compiler output>" }. This endpoint is meant for tooling such as the desktop IDE; prefer /api/tasks for anything interactive, since it does not hold a connection open for the whole build.

Project versions

A version pins a project's configuration and carries the compiled artifacts:

  • binary — the application/firmware image served as a .tcu OTA download
  • releaseBinary — an optional release image served as a .bin download
  • url / releaseUrl — absolute download URLs generated by the server

List or get versions

GET /api/projects/:id/versions
GET /api/projects/:id/versions/:versionCode

The list form returns metadata only (no binaries):

{
"count": 4,
"data": [
{
"_id": "664a1f9c8c9d0e0011223377",
"name": "Boiler control",
"versionCode": "13",
"versionDescription": "Add second zone",
"version": "3",
"device": { /* … */ },
"createdAt": "…",
"url": "https://api.appblocks.io/api/projects/…/firmware/….tcu",
"releaseUrl": "https://api.appblocks.io/api/projects/…/release/….bin"
}
]
}
note

For the single-version form, :versionCode is the version document's _id — not its versionCode field. Download URLs, by contrast, use the versionCode.

Create a version

POST /api/projects/:id/versions
{
"versionCode": "13",
"versionDescription": "Add second zone",
"name": "Boiler control",
"description": "…",
"shortDescription": "…",
"device": { /* … */ },
"features": [ /* … */ ],
"version": "3",
"runtime": "tios"
}

versionCode must be unique within the project. Returns the created version. Requires edit rights on the project.

Upload binaries

PUT /api/projects/:id/versions/:versionCode
Content-Type: multipart/form-data
FieldTypeNotes
binaryfileRequired. The firmware image
releaseBinaryfileOptional release image
firmwareVersiontextUsed in the generated filename and stored on the version
runtimetextTarget runtime
workspace, project, nametextAccepted by the upload handler

Here :versionCode is the version's versionCode field. The server generates the download URLs and returns 204 No Content:

<API_URL>/api/projects/<projectId>/versions/<versionCode>/firmware/<projectId>_<versionCode>_<firmwareVersion>.tcu
<API_URL>/api/projects/<projectId>/versions/<versionCode>/release/<projectId>_<versionCode>_<firmwareVersion>.bin

Feed the first URL to device firmware assignment or to group firmware assignment.

Delete a version

DELETE /api/projects/:id/versions/:versionCode

:versionCode is the version's _id. Requires edit rights on the project.

Downloads

GET /api/projects/:id/versions/:versionCode/firmware/:filename   # OTA image (.tcu)
GET /api/projects/:id/versions/:versionCode/:filename # application binary
GET /api/projects/:id/versions/:versionCode/release/:filename # release image (.bin)

All three are public so devices can fetch updates without credentials, and all respond with application/octet-stream. The :filename only sets the download name; the content is determined by the project and version.

The OTA endpoint streams in 32 KB chunks and logs progress server-side, using the requesting IP (or the matching device's MAC) to label the transfer — this is what produces the OTA update <mac> - <percent>% lines in the server log. The plain binary endpoint trims everything before the TBIN marker so the response is the bare application image.

Example projects

GET /api/projects/examples
GET /api/projects/examples/categories
GET /api/projects/examples/:id

Examples are read from the server's public/examples directory. /examples returns { data, count } where each entry has id, name, device, description, image and any extra metadata from the example's project.json. /examples/categories returns an array of categories, each with a projects array of { id, name }. /examples/:id adds the rendered README.md as content and the full project configuration as project, ready to be posted to /api/projects.

Workspace bundles

Bundles move a whole workspace — projects, versions with firmware, device groups, workflows, dashboards and SCADA screens — between servers or workspaces.

Export

POST /api/workspace-projects/export
{
"workspaceId": "664a1f2b8c9d0e0011223344",
"bundleVersion": "2026.08.1",
"includeBinaries": true
}

Requires workspace membership. The response is a JSON bundle sent as a file attachment:

{
"_exportVersion": "1.1.0",
"_bundleId": "664a1f2b8c9d0e0011223344",
"_bundleVersion": "2026.08.1",
"_exportedAt": "2026-08-17T06:00:00.000Z",
"_sourceServerUrl": "https://api.appblocks.io",
"_sourceWorkspace": { "id": "664a1f2b…", "name": "Acme Facilities" },
"workspace": { "name": "Acme Facilities", "variables": { "MQTT_HOST": "__PLACEHOLDER__" }, "tenantVariableTemplates": [] },
"projects": [],
"projectVersions": [],
"deviceGroups": [],
"workflows": [],
"pageApps": [],
"scadaScreens": []
}

Workspace variable values are replaced with __PLACEHOLDER__ so secrets do not travel with the bundle; set them again after import. Firmware binaries are base64-encoded into the bundle unless includeBinaries: false, in which case snapshots carry metadata only and their download URLs will not resolve on the target server. Devices, tenants and members are never exported.

Import

POST /api/workspace-projects/import
{
"bundle": { /* exported JSON */ },
"workspaceName": "Acme Facilities (copy)",
"workspaceId": "664b9a1c8c9d0e00112255aa"
}

With workspaceId, the bundle's contents are added to that existing workspace (requires edit rights). Without it, a new workspace is created and owned by the caller. Fresh ids are generated for every entity while cross-references are preserved, and firmware download URLs are rewritten to the importing server.

{
"message": "Workspace project imported successfully",
"workspace": { /* … */ },
"importedIntoExisting": false,
"counts": { "projects": 3, "projectVersions": 7, "deviceGroups": 2, "workflows": 4, "pageApps": 1, "scadaScreens": 0, "tenantVariableTemplates": 2 }
}

Update from a newer bundle

POST /api/workspace-projects/update
{
"bundle": { /* newer export of the same bundle */ },
"workspaceId": "664b9a1c8c9d0e00112255aa",
"removeOrphans": false
}

Entities are matched by their _bundleSourceId, so this behaves like an upgrade rather than a duplicate import: matched entities are replaced in place (keeping their local _id, workspace and owner), new entities are inserted, and entities whose source is no longer in the bundle are deleted when removeOrphans is true — otherwise left untouched. Project versions also match on their natural sourceProject + versionCode key, so snapshots created before bundle tracking still line up.

A bundle without _bundleId cannot be matched and returns 400.

{
"message": "Workspace updated from bundle successfully",
"workspace": { /* … */ },
"orphansRemoved": false,
"counts": {
"projectsUpdated": 3, "projectsInserted": 1, "projectsOrphaned": 0,
"deviceGroupsUpdated": 2, "deviceGroupsInserted": 0, "deviceGroupsOrphaned": 1
}
}