Node Model & Feature Registration
Every workflow node is defined inside a feature worker — an independent service (e.g. continuum-feature-base, continuum-feature-ai, continuum-feature-cheminformatics, or your own) built with the continuum-worker-springboot-starter. Workers register their nodes centrally so the orchestration service knows which worker to dispatch each node to.
Defining a node
Every node:
- Is annotated
@ContinuumNode(not just@Component) — enforced at build time by a Gradle validation task and at startup by the node activity handler. - Extends
ProcessNodeModel(data nodes) orTriggerNodeModel(source nodes). - Overrides
inputPorts,outputPorts, andmetadata(asContinuumWorkflowModel.NodeData). - Implements
execute()— the simple overload for most nodes, or theExecutionContextoverload for nodes that need credentials.
@ContinuumNode
class MyNodeModel : ProcessNodeModel() {
override val inputPorts = mapOf("input" to ContinuumWorkflowModel.NodePort(...))
override val outputPorts = mapOf("output" to ContinuumWorkflowModel.NodePort(...))
override val metadata = ContinuumWorkflowModel.NodeData(...)
override fun execute(
properties: Map<String, Any>?,
inputs: Map<String, NodeInputReader>,
nodeOutputWriter: NodeOutputWriter,
nodeProgressCallback: NodeProgressCallback,
executionContext: ExecutionContext // only needed for credential access
) { /* ... */ }
}
Node documentation is auto-loaded from resources/<package>/<ClassName>.doc.md on startup.
How a node ends up in the registry
At startup, each worker publishes its available nodes to Kafka so the platform can build a central registry of "which node lives on which worker":
┌──────────────────────┐ Avro binary ┌──────────────────────┐ PostgreSQL
│ Worker (startup) │ ──── Kafka topic ─────► │ Message Bridge │ ────► registered_nodes
│ Discovers all │ FeatureRegistration │ Consumes & upserts │ (nodeId + taskQueue)
│ @ContinuumNode │ │ per node │
│ beans via Spring │ │ │
└──────────────────────┘ └──────────────────────┘
- On
ApplicationReadyEvent, the worker iterates everyContinuumNodeModelbean it has. - For each node, it builds a
FeatureRegistrationRequest— containingnodeId(fully-qualified class name),workerId,featureId,taskQueue, a JSONnodeManifest(ports, schemas, title, icon), and the node's documentation markdown — and publishes it to Kafka (Avro, keyed bynodeId). continuum-message-bridgeconsumes the message and performs an atomic upsert into theregistered_nodestable.- The orchestration service's
InitializeActivitylater queries this registry (via the API server) to resolve which task queue should run each node in a workflow.
featureId is derived by stripping the trailing .node package segment from the class name, e.g. org.projectcontinuum.feature.analytics.node.CreateTableNodeModel → org.projectcontinuum.feature.analytics.
nodeManifest is kept as an opaque JSON string (not typed Avro fields) so the schema stays forward-compatible as NodeData evolves. Registration is one message per node, enabling clean idempotent upserts, and extensions (a map<string>) is reserved for future fields without breaking schema compatibility.
Credentials
Nodes that need external secrets (API keys, DB passwords) don't receive them automatically:
- The node's
propertiesUISchemadeclares a control withoptions.format = "credential"purely socontinuum-workbenchrenders a credential picker in its config dialog — the backend doesn't read this schema. - Inside
execute(), the node reads its ownpropertiesto find the configured credential name, then callsexecutionContext.getCredential<T>(name). This fetches the credential on demand fromcontinuum-credentials-server(GET /api/v1/credentials/{name}) and deserializes it intoT, throwing if not found.
Credentials are fetched lazily, per execution — never bundled into the workflow definition or passed through Kafka.