Nodes

Contract node — `models.RuleNodeType`

The decision / control-flow primitive. A Contract node runs logic just like a Code node, but its output is interpreted as a list of tags that selects which of the node's outgoing transitions (Next ent

The decision / control-flow primitive. A Contract node runs logic just like a Code node, but its output is interpreted as a list of tags that selects which of the node's outgoing transitions (Next entries) fire. Every "branch", "condition", "gate", "router", or "switch" a designer imagines is a Contract node — this is what makes the "a node with any number of outputs" case reduce to a primitive (see from-frontend.md).

Rule data

type ContractRule struct {
    Lang       string         // "js" or "opa"
    LogicRule  string
    Conditions map[string]any // arbitrary criteria data available to the rule
    OpaResult  string         // for OPA: which binding holds the resulting tag list
}

Same two languages as Code; the difference is purely in how the result is used.

// JS: the last expression must resolve to a tag list
rule := nodes.NewJsRuleLogicNode(
    nodes.WithContractLogicCode(`c = 8; if (c < data.threshold) { next = ["a"] } else { next = ["else"] }; next`),
    nodes.WithContractConditions(map[string]any{"threshold": 10}),
)

// OPA: OpaResult names the binding that holds the tag list
rule := nodes.NewOpaRuleLogicNode("next",
    nodes.WithContractLogicCode(`c = 8
     default next := ["else"]
     next := ["a", "b"] if { c < data.threshold }`),
    nodes.WithContractConditions(map[string]any{"threshold": 10}),
)

How branching actually works

  1. The rule returns a tag list, e.g. ["a"] (or ["a","b"] to fire multiple branches at once — fan-out).
  2. The engine compares that list against each Next.Tags on this node.
  3. Only the transitions whose tags match are followed.

So the branch decision lives in the rule, and the branch wiring lives in the tagged Next entries. Conditions is the static criteria data the rule reads (exposed as data.*), which is how the same rule logic can be parameterized per node.

Contract is not the only decider. Tags-on-Next is a runtime mechanism, not a Contract feature: an Extrinsic service can answer with svcHandler.FilterNextResponse(data, tags), and a running Plugin job can send next_tags (sdkv1 job.CmdNextFilter) — which is how a model node routes to the port whose function it chose. Identical filter semantics in all three cases; see ../routing.md. What is special about Contract is that its decision is a compiled, deterministic rule evaluated in-engine, with nothing external involved.

The base class for operator nodes

This is why the primitive set does not grow. Every process designer eventually wants a drawer of comparison nodes — greater than, less than, contains, is empty, matches, between — and none of them exist in the runtime. Each is a Contract whose rule the compiler hook builds from the node's form fields:

// one case per palette node; the engine learns nothing new
case "op_gte":
    n := inflowNodes.NewJsRuleLogicNode(
        inflowNodes.WithContractLogicCode(`input[data.field] >= data.value ? ["true"] : ["false"]`),
        inflowNodes.WithContractConditions(map[string]any{
            "field": data["field"], "value": data["value"],
        }),
    )
    node.Type = inflowModels.RuleNodeType
    node.Contract = &n.ContractRule

On the canvas that is a node with two inputs and a true / false handler pair — no code editor, no mention of JavaScript. The rule is authored at compile time by whoever builds the product; the end user only sees the node. Fifty operators later the runtime is unchanged, which is the concrete form of the primitives claim in from-frontend.md.

Frontend representation (inspector) — handlers = tagged outputs

This is the most instructive node to see on the canvas (ContractNode.vue + ContractDrawer.vue). Unlike other nodes, its default right-side output handle is removed. Instead the user adds any number of handler handles along the bottom, each with:

  • tags (tag1, tag2, …) — the label(s) this branch responds to, and
  • a color (cosmetic only).

Relevant node.data:

FieldMeaning
lang"js" or "opa"
logic_rulethe rule code (edited in the drawer)
opa_resultwhich OPA binding is the tag list (OPA only)
conditions[]key/value criteria → ContractRule.Conditions
handlers[]{ id, tags[], color } — one per output branch

When the user draws an edge from a handler, the edge inherits that handler's tags into edge.data.tags (see VueFlowPage.vue onConnect). At compile time each edge becomes a models.Next{NodeId, Tags}; at runtime the rule's returned tags select among them. Deleting a handler removes its edges (removeHandlerEdges).

"Three outputs: success / retry / reject" is literally three handlers, each tagged, on one Contract node — plus a rule that returns one of those tag strings.

Contract vs. Code

CodeContract
LanguagesJS / OPAJS / OPA
Output meaninga value written to contexta tag list selecting branches
Handles (frontend)one outputmany tagged handler outputs
NodeTypeCodeNodeTypeRuleNodeType

Compiles to

// inside the compiler hook, roughly:
n := inflowNodes.NewJsRuleLogicNode(inflowNodes.WithContractLogicCode(data["logic_rule"].(string)))
node.Type = inflowModels.RuleNodeType
node.Contract = &n.ContractRule
// the compiler fills node.Next from the edges, carrying each edge's Data.Tags

The compiler needs no special handling for branching — each handler is just an edge with its own tags. See ../compilers/vueflow.md.

Next

Synced from inflow-fusion/docs/nodes/contract.md in the inflow-fusion repo.