Nodes

Extrinsic node — `models.ExtrinsicNodeType`

The "call my own backend" primitive. When the engine reaches an extrinsic node it publishes to a NATS subject you registered and uses the reply as the node's output — one request in, one response out.

The "call my own backend" primitive. When the engine reaches an extrinsic node it publishes to a NATS subject you registered and uses the reply as the node's output — one request in, one response out. This is how a flow invokes your domain logic (write a row, compute something, hit an internal service) without the engine knowing anything about your storage or code.

flow reaches node ──► engine publishes to subject ──► your handler runs ──► reply = node output

Unlike a Plugin (a rich, long-running external process with its own UI and job lifecycle), an extrinsic node is deliberately thin: a plain request/reply against a subject your backend owns. Registration lives in this SDK's svcHandler; the wire-level detail is in ../infra.md.

Rule data

type ExtrinsicRule struct {
    InfraIsolated     InfraIsolated  // optional: run over an isolated account instead of default
    Subject           string         // NATS subject to publish to
    Data              map[string]any // static payload merged into the request
    ReqTimeoutSecound uint8          // default 5
}

Builder

n := nodes.NewExtrinsicSvcNode("my.internal.svc.persist.orders")

WithIsolated(...) scopes the call to a specific NATS account/space (the same spaces concept plugins use — see the README's provisioning section). If InfraIsolated.Account is empty, it defaults to the shared inflow account connection.

Registering the service side

An extrinsic node is only half the story — something must answer on the subject. You register a handler with svcHandler.ImplHandlerOnSubject(name, topic, handler):

svcHandler.ImplHandlerOnSubject("db_handler", svcHandler.SvcTopic("my.internal.svc.persist.*"),
    func(header nats.Header, data []byte) ([]byte, error) {
        table := strings.Split(header.Get("recv_subject"), ".")[4]
        saveOnTable(table, data)
        return []byte(`{"status":"saved"}`), nil // reply becomes the node's output
    })

How the mechanism works

  1. Register. ImplHandlerOnSubject subscribes on topic.ConvertToSubscribe() — every {param} placeholder becomes a NATS * wildcard. So svc.add.issue.{TABLE_NAME} subscribes as svc.add.issue.*, letting one handler serve many logical topics (one per table).
  2. Recover parameters. Before calling your handler the wrapper sets a recv_subject header to the exact subject the message arrived on; the handler splits it to recover the concrete params (e.g. the table name).
  3. Reply = output. Whatever bytes the handler returns are sent back as the reply; on error it responds {"error": "..."}. The engine uses that reply as the node's output.
  4. Local registry. Registrations are tracked in a process-local map keyed by name (svcHandler.GetSvc(name) / GetAllSvcs()), so a compiler can resolve a logical service name back to its subject pattern and fill placeholders with SvcTopic.MakeReqSubjectWithParams(args).

Reference example: inspector-api

inflow-inspector-api is itself an instance built on this platform (via inflow-fusion), which is why it registers its own extrinsic services. In inspector-api/inflow/port.goLoadSvcNodehandlers:

func LoadSvcNodehandlers() error {
    svc_sub1 := "svc.add.issue.{TABLE_NAME}"
    err := svcHandler.ImplHandlerOnSubject("exports_db", svcHandler.SvcTopic(svc_sub1),
        func(header nats.Header, data []byte) ([]byte, error) {
            subject := header.Get("recv_subject")   // the concrete subject it arrived on
            table := strings.Split(subject, ".")[3] // recover {TABLE_NAME}
            return []byte(fmt.Sprintf(`{"status":"saved successfully on %s table"}`, table)), nil
        })
    // ...
}

That single registration turns svc.add.issue.{TABLE_NAME} into a callable service named exports_db. A flow node pointed at it (with a concrete table) invokes the handler, and {"status":"..."} becomes the node's output.

Frontend representation (inspector)

Palette type extrinsic (ExtrinsicNode.vue + ExtrinsicDrawer.vue). Relevant node.data:

FieldBackendMeaning
serviceTopicSubjectthe subject to publish to (often resolved from a logical service name)
operationData{}Datastatic key/value payload merged into the request
timeoutReqTimeoutSecoundper-request timeout (seconds)

The node has one input and one output handle — it's a linear step. hasSettings lights it up (green) once any of the above is set.

Compiles to

The compiler hook reads those fields, resolves the subject (often via svcHandler.GetSvc(name) + SvcTopic.MakeReqSubjectWithParams to fill placeholders), and builds nodes.NewExtrinsicSvcNode(subject, …)models.ExtrinsicRule. See dev-backend/inflow/compiler.go's NODE_MY_A case and ../compilers/vueflow.md.

Extrinsic vs. Plugin

Extrinsic (this node)Plugin (plugin.md)
PurposeCall an internal service the backend ownsFull external adapter node
Call modelEngine publishes; handler's return is the output (req/reply)Two-phase job: jobId ack, then async progress/context/done
UINo per-node form builderOwn JSON Forms UI per action
LifecycleA plain subscription/handlerLong-running process; progress, context read/write, stop
Registered viasvcHandler.ImplHandlerOnSubject(name, topic, handler)Plugin SDK p.AddAction(...)
Node modelmodels.ExtrinsicRulemodels.PluginRule

Next

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