Project JSON Generation Guide
This guide covers everything needed to generate valid TinySystems project import JSONs programmatically — whether by hand, script, or AI model. It consolidates rules learned from real-world project generation and debugging.
Overview
A TinySystems project export is a single JSON file containing flows, nodes, edges, and optional widget pages. The platform and desktop client can import this JSON to deploy a complete project.
Project JSON
├── projectName — project identifier
├── tinyFlows[] — flow definitions (name + resourceName)
├── elements[] — nodes and edges (the graph)
└── pages[] — optional widget dashboard pagesTop-Level Structure
{
"projectName": "my-project",
"tinyFlows": [
{
"name": "Main Flow",
"resourceName": "main-flowab1cd"
}
],
"elements": [
{ "...nodes..." },
{ "...edges..." }
],
"pages": []
}projectName— human-readable project nametinyFlows[].name— display name for the flowtinyFlows[].resourceName— unique identifier used in node IDs and edge referenceselements[]— mix of nodes (tinyNode) and edges (tinyEdge) in any orderpages[]— widget dashboard pages (optional)
Node Structure
Node ID Format
Node IDs follow a strict format. The module operator parses these to determine which module owns the node:
{moduleName}.{componentName}-{suffix}Examples:
tinysystems-common-module-v0.cron-cr01
tinysystems-http-module-v0.http-server-hs01
tinysystems-googleapis-module-v0.firestore-get-docs-fg01
tinysystems-js-module-v0.js-eval-js01The suffix (e.g., cr01, hs01) must be unique within the project. Use a short mnemonic related to the component type.
Do NOT include flow prefixes in node IDs. The backend adds the flow prefix automatically during save. If you include one (e.g., deploy.tinysystems-...), the module operator won't find the correct module.
Required Node Fields
{
"data": {
"component": "cron",
"module": "tinysystems/common-module-v0",
"handles": [ "...target port handles..." ]
},
"flow": "main-flowab1cd",
"id": "tinysystems-common-module-v0.cron-cr01",
"position": { "x": 100, "y": 200 },
"type": "tinyNode"
}| Field | Required | Description |
|---|---|---|
id | Yes | Node identifier in moduleName.componentName-suffix format |
type | Yes | Must be "tinyNode" |
flow | Yes | Must match a resourceName in tinyFlows |
position | Yes | Canvas coordinates {x, y} |
data.component | Yes | Component name (e.g., "cron", "http_server") |
data.module | Yes | Module path (e.g., "tinysystems/http-module-v0") |
data.handles | No | Target port handles with configuration and schema |
Available Modules and Components
| Module | Components |
|---|---|
tinysystems/common-module-v0 | cron, ticker, inject, signal, array_split, router, noop |
tinysystems/http-module-v0 | http_server, http_client |
tinysystems/communication-module-v0 | slack_command, slack_post_message, smtp_send |
tinysystems/encoding-module-v0 | go_template, json_decode, json_encode, json_merge_patch |
tinysystems/js-module-v0 | js_eval |
tinysystems/googleapis-module-v0 | firestore_get_docs, firestore_create_doc, firestore_update_doc, calendar_events_list, calendar_events_watch |
tinysystems/k8s-module-v0 | k8s_apply, k8s_get, k8s_delete, k8s_list |
tinysystems/git-module-v0 | git_clone |
tinysystems/grpc-module-v0 | grpc_client |
Handles (Port Configuration)
Only Include Target Port Handles
This is critical. Only include handles for target (input) ports: _settings, request, in, input, start, etc.
Never include source (output) port handles: out, _control, response, error, query_result. These are generated at runtime by the component. Including them causes schema conflicts — bare Context definitions from source ports overwrite rich ones from _settings.
"handles": [
{ "id": "_settings", "type": "target", "...schema and config..." },
{ "id": "request", "type": "target", "...schema and config..." }
]Handle Schema Structure
Each target handle should include a complete schema with $ref and $defs:
{
"id": "_settings",
"label": "Settings",
"position": 0,
"rotated_position": 0,
"type": "target",
"configuration": {
"schedule": "*/5 * * * *",
"context": {
"api_key": "",
"webhook_url": ""
}
},
"schema": {
"$ref": "#/$defs/Settings",
"$defs": {
"Context": {
"configurable": true,
"configure": false,
"description": "API credentials",
"title": "Context",
"type": "object",
"properties": {
"api_key": {
"type": "string",
"title": "API Key",
"propertyOrder": 1
},
"webhook_url": {
"type": "string",
"title": "Webhook URL",
"propertyOrder": 2
}
}
},
"Settings": {
"type": "object",
"required": ["schedule"],
"properties": {
"context": {
"$ref": "#/$defs/Context",
"propertyOrder": 1
},
"schedule": {
"title": "Schedule",
"type": "string",
"default": "*/5 * * * *",
"propertyOrder": 2
}
}
}
}
}
}$defs Key Naming
The SDK title-cases type names using Go's cases.Title(), which lowercases non-initial letters in camelCase:
| Go Type | $defs Key |
|---|---|
OutputData | Outputdata |
InputData | Inputdata |
InMessage | Inmessage |
ItemContext | Itemcontext |
ScriptItem | Scriptitem |
Context | Context (single word — unchanged) |
Settings | Settings (single word — unchanged) |
Always use the SDK's title-cased keys. Mismatched keys cause UpdateWithDefinitions() to skip the merge silently.
Best Practice: Export Reference Schemas
Deploy a minimal flow with the same component types, export it, and use the exported schemas as templates. This guarantees correct $defs keys, $ref paths, and flag values.
Configurable Schema Rules
Schema Must Match Edge Configuration
This is the most important rule for configurable fields. The configurable: true schema on a _settings handle propagates to every edge from that node. It defines what the edge configuration UI shows.
The schema MUST only contain fields that downstream edge configurations actually use.
If a cron's first edge maps 3 context fields:
"context": {
"api_key": "{{$.api_key}}",
"project_id": "{{$.project_id}}",
"webhook_url": "{{$.webhook_url}}"
}Then the cron's _settings Context schema must have exactly those 3 fields — not 10. Extra fields appear on every edge from this node, confusing users who see fields they never configured.
The same rule applies to _settings configuration values — only include fields used in downstream edge configs.
Schema Definitions Must Have Type
Every $defs definition that has "properties" must also have "type": "object". Without it, the UI renders the field as raw JSON instead of a structured form.
// WRONG — has properties but no type
"Context": {
"configurable": true,
"properties": { "name": { "type": "string" } }
}
// CORRECT
"Context": {
"configurable": true,
"type": "object",
"properties": { "name": { "type": "string" } }
}Arrays Must Have Items
Every property with "type": "array" must include an "items" definition. Without it, the data generator crashes and the node enters an error state.
// WRONG — array without items
"recipients": { "type": "array", "title": "Recipients" }
// CORRECT
"recipients": {
"type": "array",
"title": "Recipients",
"items": {
"type": "object",
"properties": {
"email": { "type": "string", "title": "Email" },
"name": { "type": "string", "title": "Name" }
}
}
}configurable vs shared Flags
| Flag | Meaning | Use On |
|---|---|---|
configurable: true | User can edit via edge config, schema propagates | _settings and target port Context |
configurable: false | Explicitly not configurable | Source port Context (set by runtime) |
shared: true | Schema propagates but user cannot edit | Read-only fields like array on array_split |
| No flag | Structural definition only | Root types like Settings, Request |
Edge Structure
Required Edge Fields
{
"id": "tinysystems-common-module-v0.cron-cr01_out-tinysystems-http-module-v0.http-client-hc01_request",
"type": "tinyEdge",
"flow": "main-flowab1cd",
"source": "tinysystems-common-module-v0.cron-cr01",
"sourceHandle": "out",
"target": "tinysystems-http-module-v0.http-client-hc01",
"targetHandle": "request",
"data": {
"configuration": {
"url": "{{$.webhook_url}}",
"method": "GET",
"context": {
"api_key": "{{$.api_key}}"
}
},
"valid": true
}
}| Field | Required | Description |
|---|---|---|
id | Yes | Format: {sourceNodeId}_{sourceHandle}-{targetNodeId}_{targetHandle} |
type | Yes | Must be "tinyEdge" |
flow | Yes | Must match a resourceName in tinyFlows |
source | Yes | Source node ID |
sourceHandle | Yes | Source port name (e.g., out, response, error) |
target | Yes | Target node ID |
targetHandle | Yes | Target port name (e.g., request, in, input) |
data.configuration | Yes | Maps source data to target port fields |
data.valid | No | Set to true to skip re-validation |
Edge Configuration
The configuration object maps source data fields to target port fields using expressions. Every key must match a json struct tag on the target port's Go type.
"configuration": {
"url": "https://api.example.com/data",
"method": "GET",
"headers": {
"Authorization": "{{'Bearer ' + $.api_key}}"
},
"context": {
"project_id": "{{$.context.project_id}}"
}
}Configuration without expressions — use literal values:
"configuration": {
"contentType": "text/html",
"limit": 10
}Configuration with expressions — reference source data with {{}}:
"configuration": {
"body": "{{$.content}}",
"subject": "{{\"Report: \" + $.context.title}}"
}Edge Data Schema (Optional)
Edge schemas are derived at runtime from the target port's native schema. You can include data.schema in the import JSON, but it's optional — the platform regenerates it.
Expression Syntax
Expressions use {{expression}} syntax with JSONPath via the ajson library.
Data References
{{$.field}}— direct field access{{$.nested.field}}— nested access{{$.array[0]}}— array index{{$.context.api_key}}— context field
Common Patterns
String concatenation:
{{"Hello " + $.name}}
{{"Bearer " + $.context.api_key}}Conditional (ternary):
{{$.count > 0 ? "has items" : "empty"}}
{{$.error ? $.error : "success"}}Array functions:
{{length($.items)}}
{{first($.results)}}
{{last($.items)}}String functions:
{{lower($.name)}}
{{replace($.text, "old", "new")}}
{{contains($.url, "https")}}
{{split($.path, "/")}}
{{join($.tags, ", ")}}Expression Rules
Double quotes only — ajson does not support single quotes. In JSON, escape as
\":json"value": "{{$.count > 0 ? first($.results).token : \"\"}}"Never use
"context": "{{$}}"— this creates nested$.context.context.Xchains. Always re-assemble context with only the needed fields:json"context": { "api_key": "{{$.context.api_key}}", "project_id": "{{$.context.project_id}}" }Use json struct tags, not Go field names —
json.Unmarshalsilently drops keys that don't match:- Use
"contentType"not"ContentType"(if json tag isjson:"contentType") - Use
"refID"not"RefID"(check the actual json tag)
- Use
Context Flow Pattern
Context is the mechanism for passing credentials and metadata through a flow. It uses configurable: true to propagate schema through edges.
How Context Flows
Cron (settings: api_key, project_id)
→ out: emits {api_key: "xxx", project_id: "yyy"}
→ Edge config: context.api_key = {{$.api_key}}
→ Node A receives context
→ Edge config: context.api_key = {{$.context.api_key}}
→ Node B receives context
→ ... continues through the flowEach edge explicitly maps which context fields to carry forward. Fields not mapped are dropped.
Context Sizing Per Node
Each node's _settings Context schema should contain only the fields that its downstream edges use. Different flows need different credentials:
Flow A (API sync): api_key, project_id
Flow B (Email alerts): api_key, smtp_host, smtp_password
Flow C (Webhook): webhook_url, secretDo not give all three flows the same 5-field Context. Each gets only what it needs.
Adding Fields Downstream
Flows often add computed fields to context as data progresses:
// Cron emits: {api_key, project_id}
// After JS eval adds event_id:
"context": {
"api_key": "{{$.context.api_key}}",
"project_id": "{{$.context.project_id}}",
"event_id": "{{$.outputData.event_id}}"
}These added fields don't belong in the cron's _settings Context — they originate from downstream computation.
Widget Pages
Widget pages create dashboard views from node _control ports.
"pages": [
{
"title": "Dashboard",
"widgets": [
{
"name": "API Status",
"port": "tinysystems-http-module-v0.http-server-hs01:_control",
"schemaPatch": []
}
]
}
]Widget Port References
The port field format is {nodeId}:{portName}. The node ID must match an element in elements[].
Schema Patches
schemaPatch is a JSON Patch array applied to the _control port schema before rendering. Only include patches when the default rendering needs modification:
// Remove configure flag so widget renders as form, not JSON editor
"schemaPatch": [{"op": "remove", "path": "/$defs/Context/configure"}]Use an empty array [] when no patches are needed. Do not add patches just because other widgets have them.
Common Component Patterns
Cron (Scheduled Trigger)
{
"data": {
"component": "cron",
"module": "tinysystems/common-module-v0",
"handles": [
{
"id": "_settings",
"type": "target",
"label": "Settings",
"position": 0,
"rotated_position": 0,
"configuration": {
"context": { "api_key": "" },
"schedule": "*/5 * * * *"
},
"schema": {
"$ref": "#/$defs/Settings",
"$defs": {
"Context": {
"configurable": true,
"configure": false,
"title": "Context",
"type": "object",
"properties": {
"api_key": { "type": "string", "title": "API Key", "propertyOrder": 1 }
}
},
"Settings": {
"type": "object",
"required": ["schedule"],
"properties": {
"context": { "$ref": "#/$defs/Context", "propertyOrder": 1 },
"schedule": { "title": "Schedule", "type": "string", "default": "*/5 * * * *", "propertyOrder": 2 }
}
}
}
}
}
]
},
"flow": "main-flowab1cd",
"id": "tinysystems-common-module-v0.cron-cr01",
"position": { "x": 100, "y": 200 },
"type": "tinyNode"
}Output port: out — emits the context object on schedule.
HTTP Server (Request/Response)
The HTTP server is a blocking component — it waits for data to flow back to its response port before sending the HTTP response.
Settings: enableErrorPort, start.addr (listen address), start.readTimeout, start.readHeaderTimeout
Ports: request (output, emits incoming HTTP requests), response (input, receives response data)
Array Split (Iterator)
Splits an array into individual items, processing each through downstream nodes.
Input port: in — expects { array: [...], context: {...} }Output port: item — emits { item: <element>, context: {...} } for each array element
The Itemcontext definition on the in handle controls what properties the item output has. It must list the properties of each array element so the edge configuration UI can show them:
"Itemcontext": {
"configurable": false,
"shared": true,
"type": "object",
"properties": {
"email": { "type": "string", "title": "Email" },
"name": { "type": "string", "title": "Name" }
}
}JS Eval (JavaScript Execution)
Runs JavaScript code with access to input data. The outputData field is configurable: true — its schema must define the output structure.
Settings: enableErrorPort, outputData (configurable schema), script (JavaScript code), modules (NPM packages)
The Outputdata definition in the _settings handle must have "type": "object" and explicit properties:
"Outputdata": {
"configurable": true,
"title": "Output Data",
"type": "object",
"properties": {
"result": { "type": "string", "title": "Result", "propertyOrder": 1 },
"count": { "type": "integer", "title": "Count", "propertyOrder": 2 }
}
}Import Validation
The platform validates import JSON before creating any Kubernetes resources. Validation catches:
Errors (blocking):
- Missing required fields on nodes and edges
- Edge source/target referencing non-existent nodes
- Missing
data.configurationon edges - Config keys not matching schema properties
- Invalid expression syntax (unclosed
{{, empty expressions)
Warnings (informational):
- Source port handles included (will be skipped)
- Arrays without
itemsdefinition - Definitions with
propertiesbut missingtype: "object" - Schema missing
$refor$defs $defskey naming not matching SDK title-casing
Checklist
Before importing, verify:
- [ ] Every
flowreference matches atinyFlows[].resourceName - [ ] Node IDs use
moduleName.componentName-suffixformat (no flow prefix) - [ ] Only target port handles included (no source ports)
- [ ] Every handle schema has
$refand complete$defs - [ ]
$defskeys use SDK title-casing (OutputdatanotOutputData) - [ ] Every
$defsentry withpropertiesalso has"type": "object" - [ ] Every array property has
"items"definition - [ ] Every edge has
data.configuration - [ ] Edge config keys match json struct tags
- [ ]
_settingsContext schema has ONLY the fields used in downstream edge configs - [ ]
_settingsconfiguration values match the schema (no extra fields) - [ ] Expressions use double quotes (no single quotes)
- [ ] Context is re-assembled per edge (no
"context": "{{$}}") - [ ] Widget ports reference nodes that exist in elements
- [ ]
schemaPatchonly included when default rendering needs modification
Complete Minimal Example
A simple project with a cron triggering an HTTP request:
{
"projectName": "api-health-check",
"tinyFlows": [
{
"name": "Health Check",
"resourceName": "health-checkab1cd"
}
],
"elements": [
{
"data": {
"component": "cron",
"module": "tinysystems/common-module-v0",
"handles": [
{
"id": "_settings",
"type": "target",
"label": "Settings",
"position": 0,
"rotated_position": 0,
"configuration": {
"context": {
"url": "https://api.example.com/health"
},
"schedule": "*/5 * * * *"
},
"schema": {
"$ref": "#/$defs/Settings",
"$defs": {
"Context": {
"configurable": true,
"configure": false,
"title": "Context",
"type": "object",
"properties": {
"url": {
"type": "string",
"title": "Health Check URL",
"propertyOrder": 1
}
}
},
"Settings": {
"type": "object",
"required": ["schedule"],
"properties": {
"context": { "$ref": "#/$defs/Context", "propertyOrder": 1 },
"schedule": {
"title": "Schedule",
"type": "string",
"default": "*/5 * * * *",
"propertyOrder": 2
}
}
}
}
}
}
]
},
"flow": "health-checkab1cd",
"id": "tinysystems-common-module-v0.cron-cr01",
"position": { "x": 100, "y": 200 },
"type": "tinyNode"
},
{
"data": {
"component": "http_client",
"module": "tinysystems/http-module-v0",
"handles": [
{
"id": "_settings",
"type": "target",
"label": "Settings",
"position": 0,
"rotated_position": 0,
"configuration": {
"enableErrorPort": true,
"enableResponsePort": true
}
}
]
},
"flow": "health-checkab1cd",
"id": "tinysystems-http-module-v0.http-client-hc01",
"position": { "x": 500, "y": 200 },
"type": "tinyNode"
},
{
"data": {
"configuration": {
"url": "{{$.url}}",
"method": "GET"
},
"valid": true
},
"flow": "health-checkab1cd",
"id": "tinysystems-common-module-v0.cron-cr01_out-tinysystems-http-module-v0.http-client-hc01_request",
"source": "tinysystems-common-module-v0.cron-cr01",
"sourceHandle": "out",
"target": "tinysystems-http-module-v0.http-client-hc01",
"targetHandle": "request",
"type": "tinyEdge"
}
],
"pages": []
}This creates a cron that fires every 5 minutes, sending a GET request to the configured URL. The context contains only the one field (url) that the edge configuration uses.