Skip to content

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 pages

Top-Level Structure

json
{
  "projectName": "my-project",
  "tinyFlows": [
    {
      "name": "Main Flow",
      "resourceName": "main-flowab1cd"
    }
  ],
  "elements": [
    { "...nodes..." },
    { "...edges..." }
  ],
  "pages": []
}
  • projectName — human-readable project name
  • tinyFlows[].name — display name for the flow
  • tinyFlows[].resourceName — unique identifier used in node IDs and edge references
  • elements[] — mix of nodes (tinyNode) and edges (tinyEdge) in any order
  • pages[] — 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-js01

The 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

json
{
  "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"
}
FieldRequiredDescription
idYesNode identifier in moduleName.componentName-suffix format
typeYesMust be "tinyNode"
flowYesMust match a resourceName in tinyFlows
positionYesCanvas coordinates {x, y}
data.componentYesComponent name (e.g., "cron", "http_server")
data.moduleYesModule path (e.g., "tinysystems/http-module-v0")
data.handlesNoTarget port handles with configuration and schema

Available Modules and Components

ModuleComponents
tinysystems/common-module-v0cron, ticker, inject, signal, array_split, router, noop
tinysystems/http-module-v0http_server, http_client
tinysystems/communication-module-v0slack_command, slack_post_message, smtp_send
tinysystems/encoding-module-v0go_template, json_decode, json_encode, json_merge_patch
tinysystems/js-module-v0js_eval
tinysystems/googleapis-module-v0firestore_get_docs, firestore_create_doc, firestore_update_doc, calendar_events_list, calendar_events_watch
tinysystems/k8s-module-v0k8s_apply, k8s_get, k8s_delete, k8s_list
tinysystems/git-module-v0git_clone
tinysystems/grpc-module-v0grpc_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.

json
"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:

json
{
  "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
OutputDataOutputdata
InputDataInputdata
InMessageInmessage
ItemContextItemcontext
ScriptItemScriptitem
ContextContext (single word — unchanged)
SettingsSettings (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:

json
"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.

json
// 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.

json
// 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

FlagMeaningUse On
configurable: trueUser can edit via edge config, schema propagates_settings and target port Context
configurable: falseExplicitly not configurableSource port Context (set by runtime)
shared: trueSchema propagates but user cannot editRead-only fields like array on array_split
No flagStructural definition onlyRoot types like Settings, Request

Edge Structure

Required Edge Fields

json
{
  "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
  }
}
FieldRequiredDescription
idYesFormat: {sourceNodeId}_{sourceHandle}-{targetNodeId}_{targetHandle}
typeYesMust be "tinyEdge"
flowYesMust match a resourceName in tinyFlows
sourceYesSource node ID
sourceHandleYesSource port name (e.g., out, response, error)
targetYesTarget node ID
targetHandleYesTarget port name (e.g., request, in, input)
data.configurationYesMaps source data to target port fields
data.validNoSet 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.

json
"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:

json
"configuration": {
  "contentType": "text/html",
  "limit": 10
}

Configuration with expressions — reference source data with {{}}:

json
"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

  1. Double quotes only — ajson does not support single quotes. In JSON, escape as \":

    json
    "value": "{{$.count > 0 ? first($.results).token : \"\"}}"
  2. Never use "context": "{{$}}" — this creates nested $.context.context.X chains. Always re-assemble context with only the needed fields:

    json
    "context": {
      "api_key": "{{$.context.api_key}}",
      "project_id": "{{$.context.project_id}}"
    }
  3. Use json struct tags, not Go field namesjson.Unmarshal silently drops keys that don't match:

    • Use "contentType" not "ContentType" (if json tag is json:"contentType")
    • Use "refID" not "RefID" (check the actual json tag)

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 flow

Each 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, secret

Do 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:

json
// 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.

json
"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:

json
// 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)

json
{
  "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:

json
"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:

json
"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.configuration on edges
  • Config keys not matching schema properties
  • Invalid expression syntax (unclosed &#123;&#123;, empty expressions)

Warnings (informational):

  • Source port handles included (will be skipped)
  • Arrays without items definition
  • Definitions with properties but missing type: "object"
  • Schema missing $ref or $defs
  • $defs key naming not matching SDK title-casing

Checklist

Before importing, verify:

  • [ ] Every flow reference matches a tinyFlows[].resourceName
  • [ ] Node IDs use moduleName.componentName-suffix format (no flow prefix)
  • [ ] Only target port handles included (no source ports)
  • [ ] Every handle schema has $ref and complete $defs
  • [ ] $defs keys use SDK title-casing (Outputdata not OutputData)
  • [ ] Every $defs entry with properties also has "type": "object"
  • [ ] Every array property has "items" definition
  • [ ] Every edge has data.configuration
  • [ ] Edge config keys match json struct tags
  • [ ] _settings Context schema has ONLY the fields used in downstream edge configs
  • [ ] _settings configuration values match the schema (no extra fields)
  • [ ] Expressions use double quotes (no single quotes)
  • [ ] Context is re-assembled per edge (no "context": "&#123;&#123;$&#125;&#125;")
  • [ ] Widget ports reference nodes that exist in elements
  • [ ] schemaPatch only included when default rendering needs modification

Complete Minimal Example

A simple project with a cron triggering an HTTP request:

json
{
  "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.

Build flow-based applications on Kubernetes