Workflow Registration#

This page covers how a resource-mode middleware instance declares the Workflows it exposes through the knowledge graph. It describes the registration decorators, the ontology prerequisites they depend on, and how the middleware materializes Capability instances and SHACL shapes from your code. For calling Workflows — dispatch, event triggers, and operation status — see Operation Coordination.

Terms this page assumes

Ontology Types Must Pre-Exist#

The middleware never creates ontology classes. Every type referenced at registration time must already exist in the ontology as an OWL class with the correct IRI. This includes:

  • A domain-specific subclass of svc:Service for the middleware instance itself

  • A domain-specific subclass of svc:Workflow for each Workflow you register

  • A subclass of cfc:Capability that the Workflow realizes

Registration decorators accept IRIs to these classes, not Python types. If the middleware cannot find a referenced class in the graph at startup, it fails immediately with an error indicating which class was missing. You cannot register a Workflow against a Capability type that does not exist, and you cannot omit the workflow_class parameter expecting the middleware to derive one.

This requirement ensures that hundreds of identical resource instances (many doors, many controllers) all share the same Workflow and Capability classes rather than minting duplicates at runtime. Author these classes once, before any middleware instance starts.

Registration Decorators#

A Workflow is registered by decorating a Python function with @mw.workflow(). The decorator requires the Capability class IRI and the Workflow class IRI as keyword-only arguments:

from kapps_semantic_middleware import SemanticMiddleware

mw = SemanticMiddleware(
    mode="resource",
    resource_iri=EX.DoorController1,  # IRI, not an object
    service_class=EX.DoorService,     # Must pre-exist in ontology
)

@mw.workflow(
    capability_class=EX.DoorOpenCapability,   # Must pre-exist in ontology
    workflow_class=EX.DoorOpenWorkflow,       # Must pre-exist in ontology
)
def open_door(direction: str) -> bool:
    """Open the door in the specified direction."""
    ...

Both capability_class and workflow_class are required in resource mode. Omitting capability_class raises a ValueError. The decorators @mw.workflow and @mw.state are resource-mode only — calling either on a middleware instance in any other mode raises a RuntimeError.

The middleware performs two actions when this decorator runs:

  1. It creates a Capability instance automatically from the pre-existing Capability type. You never instantiate Capabilities by hand — one instance is created per running process the moment the Workflow is registered.

  2. It registers the Workflow instance, linking it to the Capability via svc:realizedByWorkflow.

Every Workflow realizes exactly one Capability. An Operation addressed to that Capability resolves to this Workflow through the chain Operation → implementsCapability → realizedByWorkflow. See Operation Coordination for how Operations are dispatched and executed.

State Property Registration#

The sibling decorator @mw.state registers a readable and/or settable state as a GET-only REST endpoint:

@mw.state(
    capability_class=EX.DoorStatusCapability,
    state_property_class=EX.DoorOpenState,
    name="door_open_status",  # Optional
)
def get_door_status() -> bool:
    """Return whether the door is open."""
    ...

It takes capability_class, state_property_class, and an optional name. Like Workflows, the Capability instance is created automatically at registration. The live value is never written to the graph — only the stable svc:endpoint triple is written at registration time. Peers read the current value by invoking the endpoint, not by querying the graph.

Signature-Derived SHACL Shapes#

The middleware generates SHACL shapes describing the Workflow’s precondition (arguments) and outcome (return value) from the function’s type hints. These shapes are attached to the Workflow class (sh:targetClass), not to individual Workflow instances. All resource instances sharing the same Workflow class share the same shape.

@mw.workflow(
    capability_class=EX.DoorOpenCapability,
    workflow_class=EX.DoorOpenWorkflow,
)
def open_door(direction: str) -> bool:  # Type hints become SHACL shape
    ...

The precondition shape describes the arguments the underlying Python function requires. The outcome shape describes the return value. Both are derived automatically — you do not author SHACL by hand for workflow signatures.

Arguments are supported: build_workflow_shape introspects the signature and mints an argument property per parameter ({workflow_class_iri}#param_{name}) plus a #return property. A function with no arguments produces an empty precondition shape, which is valid. Zero-argument functions are what the test suite exercises most heavily, but that is a statement about coverage rather than a capability limit; the open_door(direction: str) example above works. Complex nested types may not yet be fully exercised.

Address and Endpoint#

When a resource-mode middleware starts, it writes two kinds of location metadata to the graph:

  • svc:address on the Service individual — the base URL of this middleware instance

  • svc:endpoint on each Workflow and StateProperty individual — the full, directly callable URL for that specific entry point

Both are written at registration time. Both are removed when the middleware deregisters (on clean shutdown) or when a watchdog marks the Service stale (on heartbeat failure). Confusing these two is a common mistake: callers resolve a Workflow IRI and read its svc:endpoint directly — they do not walk through the Service’s svc:address and append a route convention.

Storing the endpoint on the Workflow itself means any caller with just a Workflow IRI can invoke it with one property read. The trade-off is duplication: the same host and port appear in both svc:address and every svc:endpoint under that Service.

Vocabulary Layering#

Your domain ontology must fit into a three-module stack:

  • cfc: (Core) — Published, external, superior. Includes cfc:Operation, cfc:Capability, cfc:Resource, cfc:Task. Import and specialize Core; never modify it. Possession uses Core’s cfc:hasPossessor / cfc:hasPossessedWorkpiece directly.

  • mes: (MES) — Domain-facing. This is where your manufacturing-execution vocabulary lives. Domain experts author terms here. Currently carries handover-ability predicates (mes:hasHandoverAbility and its enumerated individuals).

  • svc: (Service) — Middleware-facing. Includes svc:Service, svc:Workflow, svc:StateProperty, svc:address, svc:endpoint, svc:lastHeartbeat, and Operation status/provenance vocabulary. Domain code does not touch this layer.

When you introduce new domain terms (a new device type, a new capability kind), author them in mes:. Reference Core types from cfc:. Never add domain vocabulary to svc: — that module is reserved for reachability and coordination state only.

Your Workflow and Capability classes will typically be mes: subclasses that specialize Core patterns, while their runtime instances and endpoints live in svc:.