The entity stack¶
Every gobpm process is a small tower of types. At the bottom is
foundation.BaseElement — an id and its documentation. Above it, flow.Element
adds a name and a container; flow.Node adds sequence-flow wiring; the activity,
event, and gateway families specialise Node into the shapes you actually drop
on a diagram. A process.Process holds those nodes; the engine turns the
Process into an immutable launch template and runs live instances off it, all
driven by the Thresher. This page walks that stack from bottom to top and names
the public contract at each floor — so you know what you get for free (ids, docs,
name, wiring) and what a given interface obliges you to provide.
This is a concept page. It grounds the public interfaces in
pkg/model/…and describes the runtime layers (Snapshot, instance, track, token) as observable behavior — those live ininternal/…and are not an API you call. The why behind each layer lives indocs/design/.
The stack at a glance¶
| Floor | Type / interface | Package | Adds |
|---|---|---|---|
| 0 | BaseElement / Identifyer, Documentator, BaseObject |
foundation |
id, documentation |
| 1 | flow.Element |
flow |
name, container membership, element type |
| 2 | flow.Node (BaseNode) |
flow |
incoming/outgoing flows, node type, Clone |
| 3 | ActivityNode · EventNode · GatewayNode |
flow |
the three connectable families |
| 4 | process.Process (a flow.Container) |
process |
holds nodes, flows, properties, data objects |
| 5 | snapshot → instance → track → token | internal/… |
the running form (behavior, not an API) |
| 6 | thresher.Thresher |
thresher |
registers, versions, starts, drives |
Floors 0–4 are the definition you build; floors 5–6 are the runtime the engine builds from it.
Floor 0 — BaseElement: id and docs¶
foundation.BaseElement is the abstract super-class almost every model type
embeds. It carries two attributes — the id and the documentation slice — and
exposes them through two tiny interfaces:
type Identifyer interface {
ID() string
}
type Documentator interface {
Docs() []*Documentation
}
type BaseObject interface {
Identifyer
Documentator
}
BaseElement implements both via ID() and Docs(). You rarely construct one
directly, but when you do it takes options and never a positional id:
func NewBaseElement(opts ...options.Option) (*BaseElement, error)
func MustBaseElement(opts ...options.Option) *BaseElement
| Option | Effect |
|---|---|
foundation.WithID(id string) |
pin the element's id instead of generating one. |
foundation.WithDoc(text, format string) |
attach a Documentation entry (format e.g. foundation.PlainText). |
If you omit WithID, the element gets one from the package generator —
foundation.GenerateID(), backed by the pluggable foundation.IDGenerator
(swap it with foundation.SetGenerator; see
Custom ID generator). So id is never your
responsibility unless you want it to be.
Floor 1 — flow.Element: name and container¶
A flow.Element is anything that can appear in a process flow. It builds on
BaseObject and adds a name plus a home:
type Element interface {
foundation.BaseObject // ID(), Docs()
foundation.Namer // Name() string
Container() Container // which container holds it
EType() ElementType // the element's flow-type
BindTo(Container) error
Unbind() error
}
The name comes from foundation.Namer (Name() string), so from Element
upward every type answers ID(), Docs(), and Name(). EType() returns a
flow.ElementType — a named string type with a Validate() method, not a bare
string.
Floor 2 — flow.Node: the connectable thing¶
Only four BPMN element kinds connect to sequence flows — activities, events,
gateways, and (in choreographies) choreography activities. Those are the Nodes.
flow.Node extends Element with flow wiring, a node type, and per-instance
cloning:
type Node interface {
Element
Incoming() []*SequenceFlow
Outgoing() []*SequenceFlow
AddFlow(*SequenceFlow, data.Direction) error
NodeType() NodeType
Node() Node // the underlying node object
Clone() (Node, error) // a per-instance copy
}
flow.BaseNode embeds BaseElement and provides all of this, so a concrete node
type embeds BaseNode and inherits Incoming/Outgoing/AddFlow/Clone for
free. Two behaviors are worth internalising:
- Flows are declaration-ordered.
BaseNodeholds incoming and outgoing flows per direction in the order they were added;Incoming()/Outgoing()return that order. Gateway routing depends on it — Exclusive picks the first true branch, Inclusive a subset — so the order in which youLinkmatters. Clonecan fail. Each running instance gets its own copy of the node graph (immutable config shared by reference, runtime state fresh, flow collections empty and rewired afterwards).Clonereturns an error because copying a node's properties can fail — a value-less property is unclonable; a node without properties never errors. This per-instance-graph model is ADR-009.
Floor 3 — the three families¶
Node splits into exactly the three connectable families, each adding one
introspection method:
type ActivityNode interface {
Node
ActivityType() ActivityType
}
type EventNode interface {
Node
Definitions() []EventDefinition
EventClass() EventClass // Start | Intermediate | End
}
type GatewayNode interface {
Node
GatewayType() GatewayType
}
| Family | Marker method | Concrete package | Reference |
|---|---|---|---|
| Activity | ActivityType() ActivityType |
pkg/model/activities |
Activities taxonomy |
| Event | EventClass() + Definitions() |
pkg/model/events |
Events taxonomy |
| Gateway | GatewayType() GatewayType |
pkg/model/gateways |
Gateways taxonomy |
You almost never implement these interfaces yourself — the concrete Service Task, Exclusive Gateway, Timer Event, etc. already do. They exist so the engine and your introspection code can treat any node uniformly and still ask "what kind of activity/event/gateway is this?".
Nodes are joined by sequence flows. flow.SequenceFlow is itself a
BaseElement; you create one with flow.Link:
func Link(
src SequenceSource,
trg SequenceTarget,
flowOptions ...options.Option,
) (*SequenceFlow, error)
Link also adds the new flow to the source node's container automatically. Its
options are foundation.WithID, foundation.WithDoc, options.WithName, and
flow.WithCondition (a conditional flow). See
Sequence flows & associations.
Floor 4 — Process: the container¶
A process.Process is the top of the definition. It embeds
foundation.BaseElement (so it too has an id and docs) and is a flow.Container
— the thing nodes and flows live in:
func New(name string, procOpts ...options.Option) (*Process, error)
The one option family a Process takes is data.WithProperties(...) — its
process-level properties. Its public surface is a
container plus a few catalogs:
| Method | Returns |
|---|---|
Add(e flow.Element) error / Remove(e flow.Element) error |
container mutation |
Elements() []flow.Element |
everything in the process |
Nodes(types ...flow.NodeType) []flow.Node |
nodes, optionally filtered by type |
Flows() []*flow.SequenceFlow |
the sequence flows |
Properties() []*data.Property |
process-level data |
DataObjects() / DataStoreReferences() |
scoped data containers |
Validate() error |
structural check before hand-off to the engine |
A Process is inert — a validated graph of typed nodes. It carries no runtime
state; running it is the engine's job. See
Process, instance, track, token.
Floors 5–6 — the runtime (behavior, not an API)¶
When you hand a Process to the engine, four runtime shapes appear. They live in
internal/… — you observe them, you do not construct them:
- Snapshot — the
Processis converted once into an immutable launch template: a validated, ready-to-clone node graph. It is not a persistence or recovery mechanism — the durable record is the Repository's checkpoint document, not the snapshot (Persistence & recovery). - Instance — each start
Clones the snapshot's node graph into a private, mutable copy. The immutable header (process id/name, properties, correlation keys) is shared by reference across instances; only the per-instance graph is copied. You interact with a running instance through athresher.InstanceHandle, never the internal struct. - Track — a running instance advances along one or more concurrent tracks (a token's path through the graph). Tracks are how parallel branches and boundary interruptions are expressed.
- Token — the unit of control-flow position. Splits and joins at gateways move tokens; a track carries them.
The layering — model → snapshot → instance → engine — and the single-writer execution model are described in Architecture overview and How a process executes; the rationale is ADR-001.
At the top sits the Thresher — the engine. It is the only floor with a rich
public method set (register, version, start, observe, drive):
func New(id string, opts ...Option) (*Thresher, error)
| Method | Role |
|---|---|
RegisterProcess(p *process.Process, opts ...RegisterOption) |
register a definition (versioned). |
StartLatest(key) / StartVersion(key, v) |
start an instance, returning an *InstanceHandle. |
Run(ctx) / Shutdown(ctx) |
engine lifecycle. |
Instance(id) / Instances(query) |
discover running instances. |
Observe(o Observer) |
subscribe to runtime facts. |
The full engine surface — every With* option and lifecycle method — is its own
page: The engine (Thresher) and the
Engine options catalog.
Reading the stack in code¶
Because each floor embeds the one below, a concrete node satisfies every interface down the tower at once. A single value can be asked its id, docs, name, node type, and family:
var n flow.Node = someServiceTask // *activities.ServiceTask
_ = n.ID() // foundation.Identifyer
_ = n.Name() // foundation.Namer
_ = n.Docs() // foundation.Documentator
_ = n.NodeType() // flow.Node
_ = n.Outgoing() // flow.Node — declaration-ordered
if a, ok := n.(flow.ActivityNode); ok {
_ = a.ActivityType() // it's an activity — which kind?
}
That embedding is the whole point of the stack: you build with concrete types, but the engine — and your own introspection — works against the small interfaces each floor contributes.
See also¶
- Related guides: Architecture overview · Process, instance, track, token · How a process executes · Foundation elements · The engine (Thresher)
- Design: ADR-009 — per-instance node graph · ADR-001 — execution model · SAD-001 — vision & architecture
- Full API:
go doc github.com/dr-dobermann/gobpm/pkg/model/foundation·go doc github.com/dr-dobermann/gobpm/pkg/model/flow·go doc github.com/dr-dobermann/gobpm/pkg/model/process