Rensei docs
Editor

Port Types

Typed ports, connection validation, and scope isolation.

Every edge on the canvas is validated against a type system before it is accepted. Port types let the editor enforce correctness at authoring time - if two nodes cannot be wired together, the connection is rejected immediately with an explanation, rather than failing silently at runtime.

Port definitions

Each node type declares a list of PortDefinition entries - one per input or output handle:

interface PortDefinition {
  id: string            // handle ID (e.g., 'output', 'true', 'false', 'timeout')
  direction: 'input' | 'output'
  type: PortType
  label?: string        // display label (defaults to id)
  required?: boolean    // whether this input must be wired for the node to be valid
}

When a node has no port definitions, all its handles are treated as any - accepting any connection. This maintains backward compatibility with older node types that predate the type system.

Port type kinds

The type algebra supports eight kinds:

KindDescriptionExample
anyUntyped - accepts any connectionLegacy / passthrough nodes
stringPlain text valuelinear.text.strip_mentions output
numberNumeric valueagent.dispatch_count.increment output
booleanTrue/false valueCondition node output
objectJSON Schema constrained objectlinear.issue.read output
arrayTyped array (element type declared)parallel.fanout branch list
unionOne of several types (A or B or C)Mixed-type outputs
nullExplicit null (for null propagation)Optional outputs with no value
structNominal - references a workspace struct typeCustom-typed boundary ports

Nullable ports

Any port type can be marked nullable: true. A nullable port can carry either its declared type or null. Connecting a nullable output to a non-nullable input is allowed but may trigger null propagation at runtime if the value is actually null.

Struct ports

struct ports reference a workspace-scoped struct type by id. Two struct ports are compatible only when they reference the same struct type (by ID). If both ports declare a version, the versions must match; one versioned and one unversioned port is accepted.

Struct-typed ports cannot be used on group bundle interface ports that are published to the marketplace. The platform rejects the publish with an error because StructRef IDs are workspace-scoped and cannot be resolved in the consumer's workspace.

Convenience constructors (DEV)

Node definitions use the PortTypes helper for concise declarations:

import { PortTypes } from '@/lib/workflow/port-types'

// Primitives
PortTypes.Any      // { kind: 'any' }
PortTypes.String   // { kind: 'string' }
PortTypes.Number   // { kind: 'number' }
PortTypes.Boolean  // { kind: 'boolean' }
PortTypes.Null     // { kind: 'null' }

// Compound types
PortTypes.Object(schema?)         // { kind: 'object', schema }
PortTypes.Array(PortTypes.String) // { kind: 'array', elementType: string }
PortTypes.Union(PortTypes.String, PortTypes.Number) // union
PortTypes.Nullable(PortTypes.String)  // { kind: 'string', nullable: true }
PortTypes.Struct({ id: 'my-struct-id', name: 'MyType' })

Connection validation rules

When you drag an edge from one node to another, the platform checks these rules in order. The first failing check blocks the connection:

Structural rules

  1. No self-loops - a node cannot connect to itself.
  2. Trigger nodes cannot have incoming edges - triggers are always start nodes.
  3. Annotation nodes cannot be connected - annotations are canvas decorations only.
  4. Single-input invariant - non-fanin nodes accept at most one incoming edge per input handle. Attempting to wire a second edge to the same input is rejected.
  5. Condition node outputs - in binary mode, a condition node may have exactly two outgoing edges (true and false). In switch mode, one edge per declared case plus an optional default.

Type compatibility rules

  1. Port type matching - the output port type must be compatible with the input port type. The compatibility rules are:
    • any is compatible with everything (bidirectional).
    • Primitive types (string, number, boolean) must match exactly, unless one end is any.
    • object ports are compatible when the output schema is a structural subset of the input schema (output provides at least what input requires).
    • union output is compatible with an input if any union member is compatible with the input type.
    • struct ports must reference the same struct type ID (and matching version if both are versioned).
    • A nullable output is accepted by a non-nullable input, but the runtime may propagate null downstream if the value is null at execution time.

If a connection is incompatible, the drag fails with a tooltip showing the specific type mismatch (e.g., "Cannot connect String → Number").

Scope isolation

Within a workflow that has group containers, edges are additionally constrained by scope rules:

  • An edge may only be created within a single scope. You cannot draw a direct edge from a node inside a group to a node outside (or vice versa) - you must use the group's boundary interface ports.
  • Boundary ports act as typed gateways: a node inside the group connects to the group's input boundary port, and the boundary output port connects to nodes in the parent scope.
  • Attempting to wire across a scope boundary directly shows the error: "Cannot connect across scope boundary - use interface ports".

This rule is enforced both at drag time (in the canvas) and at save time (in the canvas-to-definition validator). Both layers must agree for the workflow to be valid.

Validation at save time

When you publish or export a workflow, the platform runs a full connection validation pass (validateWorkflowFull) over all edges. Errors block the publish and are surfaced in the broken-state banner. Individual edge errors show inline on the canvas with red highlights.

You can also trigger manual validation from the workflow controls menu (Validate canvas) without publishing.

Human-readable port labels

The portTypeLabel() utility converts a PortType to a display string used in error messages and the node preview popover:

TypeLabel
{ kind: 'any' }Any
{ kind: 'string' }String
{ kind: 'object', schema: { title: 'Issue' } }Issue
{ kind: 'array', elementType: string }String[]
{ kind: 'union', members: [string, number] }String | Number
{ kind: 'struct', structRef: { name: 'Ticket' } }Ticket

Example: typed trigger output

Here is how a linear.issue.assigned trigger node might declare its output port:

// Simplified from src/lib/nodes/trigger/linear.issue.assigned/backend.ts
ports: [
  {
    id: 'output',
    direction: 'output',
    type: PortTypes.Object({
      $id: 'rensei.trigger.linear.IssueAssignedPayload',
      title: 'IssueAssignedPayload',
      properties: {
        issue: { type: 'object' },
        actor: { type: 'object' },
      },
    }),
  },
]

A downstream action node that declares its input as PortTypes.Any will accept this connection. A node expecting PortTypes.String will be rejected at drag time.

On this page