Golang API

Introduction
External agents in Nodlin communicate with the platform over NATS using JSON. They register the node types they own, receive actions and events, and issue commands back to Nodlin (store nodes, create links, raise events, run command sequences, and more).
To simplify that work, Nodlin provides a Go library β nodlinGo (github.com/johnha/nodlinGo/v2) β that covers:
- Agent registration and lifecycle
- Typed node specifications (actions, events, relationships)
- Handler context for reading and updating nodes
- Forms (vueform JSON for the web client)
- Node images (SVG / PNG)
- Checkpoints, links, out-of-context command sequences
- Assignments, FYIs, activity reporting, and user notifications
Reference agents such as FRED and OpenAI show real usage of the same library.
Related documentation
- Python / Starlark scripting β sandboxed script agents (no external I/O)
- Forms guide β form concepts (script-oriented; Go uses the same vueform model)
- Node images β visual design for nodes
- Propagation of change β checkpoints, parallelism, quiescence
- Actions and FYIs β core collaboration model
When to use an external Go agent
Nodlin supports several ways to define behaviour. Choose deliberately:
| Approach | Language | Access | Best for |
|---|---|---|---|
| External Go agent | Go + nodlinGo | External systems over NATS/JSON | APIs, AI, databases, multi-node synthesis, long-running work |
| External Python agent | Python | Same external model | Teams standardising on Python |
| Internal agent | Go (cluster) | Direct store / internal bus | Shared platform primitives (workflow, notes, files) |
| Script package | Starlark | Sandboxed Nodlin API only | Fast domain packs without external I/O |
Use external Go when you need to:
- Call HTTP APIs, AI providers, or internal services
- Keep secrets and network access outside the script sandbox
- Run work that may take seconds or minutes
- Own a set of node types that integrate tightly with those systems
Prefer scripts when logic is pure graph behaviour, forms, and visuals with no external calls.
Prefer internal workflow types (task, question, option, decision, β¦) when the concept is already a first-class platform type users know.
Behaviour is the same across agent kinds: typed nodes, forms, relationships, actions, and events. Only the API surface differs.
Core model: a reactive graph
Nodlin is a reactive knowledge graph. There is no central orchestrator that walks your business process.
- A node is stored (created or updated).
- Nodlin dispatches events to related dependents.
- Each dependent recomputes its own state and may store again.
- Change cascades through topology until the graph is quiet (consistency at quiescence).
Design implications:
- Prefer a small number of meaningful types over deep custom control-flow code.
- Edge direction is dependency direction:
A β Bmeans A cares about B (A receives events when B changes). Physical or causal flow is often the reverse. - Do not invent a separate propagation engine. Relations + events are the engine.
- Cycles are allowed; handlers should converge (skip
StoreNodewhen nothing material changed).
Architecture overview
βββββββββββββββββββ NATS / JSON ββββββββββββββββββββββββ
β External Go βββββββββββββββββββββββΊβ Nodlin extAgent β
β agent process β register, actions, β (platform bridge) β
β (nodlinGo) β events, callbacks ββββββββββββ¬ββββββββββββ
βββββββββββββββββββ β
βΌ
Nodlin core / graphs
At startup the agent:
- Connects to the agent NATS bus.
- Publishes an
ExtAgentSpec(manifest of types, actions, events, relations). - Receives an external agent id and heartbeats.
- Subscribes to action/event subjects for its handlers.
- Calls back into Nodlin for stores, links, checkpoints, command sequences, etc.
All handler work is framed by a request context (user, graph, expanded related nodes, trace). In-context callbacks are only valid while that handler is running.
Getting started
Runtime configuration
Typical flags / environment variables (names may vary slightly by agent):
| Setting | Purpose |
|---|---|
nodlin_agent_nats_server / NODLIN_AGENT_NATS_SERVER | NATS address for external agents |
hostname / HOSTNAME | Instance identity (often the pod name) |
domain_agent_id / DOMAIN_AGENT_ID | Domain agent registration id |
| Agent-specific secrets | API keys, tokens β never hardcode |
Canonical project layout
Follow a clean separation (as in the FRED agent):
~/nodlin/agents/<agent>/
<agent>.go # main: flags, ExtAgentSpec, NewNodlinAgent, shutdown
go.mod # module <agent>; require nodlinGo/v2 (+ nodlinImage if used)
Dockerfile
types/ # FQN constants + UDT structs
images/ or util/ # agent and type icons
<domainpkg>/ # handlers, forms, external clients
agentTypes.go # processor + StartProcessing(agent)
createX.go
deleteNode.go
createXForm.go
test/
Minimal main pattern
package main
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/namsral/flag"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
nlGoAgent "github.com/johnha/nodlinGo/v2/agent"
"github.com/johnha/nodlinGo/v2/nodlinAgent"
)
func main() {
var (
natsAddr string
logLevel string
hostname string
domainAgentID string
)
flag.StringVar(&natsAddr, "nodlin_agent_nats_server", "", "NATS JSON agent bus")
flag.StringVar(&logLevel, "myagent_server_loglevel", "debug", "log level")
flag.StringVar(&hostname, "hostname", "", "pod/host name")
flag.StringVar(&domainAgentID, "domain_agent_id", "", "domain agent id")
flag.Parse()
level, err := zerolog.ParseLevel(logLevel)
if err != nil {
level = zerolog.DebugLevel
}
zerolog.SetGlobalLevel(level)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
exitServer := make(chan os.Signal, 1)
signal.Notify(exitServer, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
processor, err := mypkg.NewProcessor(/* secrets from flags/env */)
if err != nil {
log.Fatal().Err(err).Msg("init processor")
}
spec := nodlinAgent.ExtAgentSpec{
Agent: "myagent",
Version: "v1",
Description: "Short description of the agent",
Image: images.AGENT_ICON,
Help: "Markdown help for operators and users",
Types: []nodlinAgent.ExtAgentTypeSpec{ /* β¦ */ },
ExecUser: "admin",
ExecDomain: "master",
}
terminated := make(chan struct{})
errChan := make(chan error, 5)
execAgent, err := nlGoAgent.NewNodlinAgent(
ctx, &spec, natsAddr, hostname, domainAgentID, errChan, terminated,
)
if err != nil {
log.Error().Err(err).Msg("register agent")
cancel()
}
processor.StartProcessing(execAgent)
for {
select {
case agErr := <-errChan:
log.Error().Err(agErr).Msg("agent error")
cancel()
case <-exitServer:
if execAgent != nil {
execAgent.Shutdown()
}
cancel()
return
case <-ctx.Done():
if execAgent != nil {
execAgent.Shutdown()
}
return
}
}
}Local development often uses go.work or replace directives to point at local nodlinGo / nodlinImage checkouts.
Agent specification
ExtAgentSpec
Top-level registration payload:
| Field | Role |
|---|---|
Agent | Short name (no special characters), e.g. fred, openai |
Version | Agent version string |
Description / Help / Image | Human-facing agent metadata |
Types | Slice of ExtAgentTypeSpec |
ExecUser / ExecDomain | Execution identity (often admin / master for platform agents) |
Validation expands short type and relation names to FQNs and initialises handler codes used on the wire.
Fully qualified names (FQN)
| Kind | Pattern | Example |
|---|---|---|
| Type | agt_<domain>_<user>_<agent>_<name> | agt_master_admin_fred_category |
| Relation | agr_<domain>_<user>_<agent>_<name> | agr_master_admin_fred_has_series |
| Property | agp_<domain>_<user>_<agent>_<name> | agp_master_admin_openai_title |
Define constants in a types package β never scatter raw FQN strings through handlers.
const (
FredDataCategory = "agt_master_admin_fred_category"
FredDataSeries = "agt_master_admin_fred_series"
FredDataSubCategory = "agr_master_admin_fred_has_subcategory"
FredDataHasSeries = "agr_master_admin_fred_has_series"
)ExtAgentTypeSpec
Every type should declare:
Name, user-facingLabel,DescriptionHelp(markdown) β short usage guide shown in the UIIconβ type picker icon; reuse in form headersActionsβ at leastcreateanddelete- Optional lifecycle actions (see below)
Relationswith human labelsEventsthe type reacts toNoDuplicate: truewhen external side-state must not be copied; or implementduplicatedto rebuild side-state after import
nodlinAgent.ExtAgentTypeSpec{
Name: types.FredDataCategory,
Label: "FRED Category",
Description: "Category of FRED data",
Icon: images.FRED_LARGE_ICON,
Help: "FRED organises data series into hierarchical categories.",
Actions: []*nodlinAgent.ExtAgentActionSpec{
{Name: "create", Description: "Create category", HandlerFunc: fred.CreateCategory},
{Name: "delete", Description: "Delete category", HandlerFunc: fred.DeleteNode},
{Name: types.FredAddCategory, Description: "Add subcategory", HandlerFunc: fred.AddCategory},
},
Relations: []*nodlinAgent.ExtAgentRelationshipSpec{
{
OverRelation: types.FredDataSubCategory,
ToType: types.FredDataCategory,
Label: "has subcategory",
InverseLabel: "parent category",
Weight: 10,
},
},
}Actions
Handlers have a single signature:
type ActionFunc func(nodlin *nodlinAgent.Nodlin) error
type EventFunc func(nodlin *nodlinAgent.Nodlin) errorReturn a non-nil error to fail the operation; nil reports success.
Required actions
| Action | When | Typical work |
|---|---|---|
create | Node is created or saved from the form | Defaults, form, image, label/summary, StoreNode |
delete | Node is deleted | Tear down side-state; often nl.Delete(ctx) |
Optional lifecycle actions
Declare only what you handle:
| Action | Fired when |
|---|---|
assigned | Assignment created on the node |
updatedAssignment | Assignment details changed |
completeAssignment | Assignment completed |
incompleteAssignment | Assignment re-opened |
fyiUpdated | FYI created or updated |
fyiAllAcknowledged | All FYIs acknowledged |
fileupdate | File attached/changed |
duplicated | Node copied (e.g. import) β rebuild side-state |
Custom action names are fine for form buttons (addCategory, chartSeries, β¦).
An action is delivered only if the type advertises it. Missing handlers are skipped (useful when an agent is offline for optional hooks).
Relationships and events
Relationships
&nodlinAgent.ExtAgentRelationshipSpec{
OverRelation: types.RelHasItem,
ToType: types.MyItem,
Label: "has item",
InverseLabel: "item of", // display only β does not create reverse reactivity
InverseRelation: types.RelItemOf, // only if the other side must react
Weight: 10,
}Rules that matter in practice:
- A declared relation is a dependency: the owning type may re-evaluate when the related node changes. Do not add reactive edges βjust in caseβ.
InverseLabelis informational (menus / reverse display). It does not by itself cause reverse re-execution.- Set
InverseRelationonly when the other node must react. For true bidirectional pairs, both types should declare the inverse so a link from either side produces both edges. - One-way reaction is intentional when only one side depends on the other (e.g. query β observation if the observation does not need query status).
- When creating links in code, always pass a human
userLabelmatching the spec label β otherwise the canvas shows the raw relation FQN.
Events
&nodlinAgent.ExtAgentEventSpec{
Name: "*", // updated + linked + deleted
FromType: types.MyItem,
OverRelation: types.RelHasItem,
HandlerFunc: p.OnItemChange,
}Guidelines:
- Default to
"*"so link, unlink/delete, and data updates all recompute. - Use a named event when intermediate
StoreNodes would thrash dependents; raise it explicitly withnl.SendEvent(ctx, "documentReady"). - Prefer specific names for expensive external recomputes.
Prefer platform constructs before new types
Before inventing a domain type, check whether Nodlin already models the concept.
| Need | Prefer |
|---|---|
| Todo / assignee / work item | Assignments on a node + lifecycle actions |
| Inform-only participants | FYIs + fyiUpdated / fyiAllAcknowledged |
| Task / question / option / decision | Internal workflow types |
| Notes / comments | Existing internal note/comment capabilities |
Example β meeting attendees: do not store an attendees array in UDT. Participants are people on assignments (and informed people on FYIs). The meeting node reads ActionList / FYIList, renders names in label/summary/image, and registers lifecycle handlers so complete/ack updates refresh the display.
Assignments and FYIs
actions, err := nl.ActionList(ctx) // or ActionListForNode
fyis, err := nl.FYIList(ctx) // or FYIListForNodeThe product UI already supports adding assignees, completion, calendar views, and FYI acknowledgement. Use those APIs and lifecycle hooks rather than reinventing todo lists in payload fields.
Internal workflow types (reuse)
Shared building blocks users already recognise (from the workflow agent), including:
| FQN | Role |
|---|---|
agt_core_all_workflowAgent_task | Task / todo-style work |
agt_core_all_workflowAgent_question | Question |
agt_core_all_workflowAgent_option | Option |
agt_core_all_workflowAgent_decision | Decision |
agt_core_all_workflowAgent_checkpoint | Checkpoint |
agt_core_all_workflowAgent_comment | Comment |
When designing an external agent:
- Ask whether a workflow type already fits.
- If it almost fits, prefer extending the internal type over a parallel external twin.
- Link external domain nodes to workflow nodes when composition is clearer than duplication.
- Use
ActionForAgentwhen creating or updating nodes owned by another agent.
Handler context: *nodlinAgent.Nodlin
Every action and event handler receives a Nodlin value: request header, expanded node context, and command helpers.
In-context vs out-of-context
| Context | When | Allowed writes |
|---|---|---|
| In-context | Inside the handler before it returns | StoreNode, Link*, Action, Checkpoint, SendEvent, β¦ |
| Out-of-context | Goroutine / worker after the handler returns | ExecuteCommandSequence, ReportNodeActivity, SendUserNotification, SendRequest |
When the handler returns, the framework ACKs success and the server closes the trace. Calling nl.StoreNode from a background goroutine after return fails with a context not found style error.
Returning without StoreNode is valid β the framework still ACKs. Unstored action payload fields are discarded.
Important methods
Request / identity
IsActionRequest,IsEventRequest,IsNewRequestActionName,RequestEventNameGetUserGraphContext,MsgHeader,NewID
Read nodes
GetNodeWithUpdatesβ central node with action UDT overlay (usual create/save path)GetCentralNodeβ node before updates (nilif new)GetRelatedActiveNodes/GetRelatedNodesGetNodeID,GetNodeAndGraphDetailInContext
Write
StoreNode,Delete/DeleteNode,Restore/RestoreNodeLink,LinkFrom,Unlink,UnlinkFromCheckpoint,Action,ActionForAgent,SendEvent
Collaboration
ActionList,ActionListForNodeFYIList,FYIListForNode
Agent-level (*agent.NodlinAgent)
ExecuteCommandSequenceReportNodeActivitySendUserNotificationShutdown,GetAgentDetail
UDT helpers
udt.NewNodeJsonUDTFromType(v)json.Unmarshal(node.UDT, &v)
Standard handler patterns
Create
func (p *Processor) CreateCategory(nl *nodlinAgent.Nodlin) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
updated, err := nl.GetNodeWithUpdates()
if err != nil {
return errors.Join(err, errors.New("load node"))
}
var data types.Category
if err := json.Unmarshal(updated.UDT, &data); err != nil {
return errors.Join(err, errors.New("decode udt"))
}
// defaults + domain logic β including recompute from related nodes
updated.Label = util.PStr(data.Name)
summary := buildSummary(data)
updated.Summary = &summary
updated.NodeSizeX = util.PFloat64(250)
updated.NodeSizeY = util.PFloat64(150)
updated.Image = /* PNG or SVG */
form, err := createCategoryForm(&data)
if err != nil {
return err
}
updated.Schema = *form
udtBytes, err := udt.NewNodeJsonUDTFromType(data)
if err != nil {
return err
}
updated.UDT = *udtBytes
return nl.StoreNode(ctx, updated.ExtAgentNode)
}Important: if event handlers derive fields from related nodes, create must apply the same derivation. Otherwise user edits look stale until some other event fires.
Delete
func (p *Processor) DeleteNode(nl *nodlinAgent.Nodlin) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return nl.Delete(ctx)
}Create β checkpoint β link
This ordered flow is extremely common for in-context child creation:
- create child (
nl.Actionβ¦"create") Checkpointso the child exists before linking- link (
Link/LinkFrom) withuserLabel - optional further checkpoint
newID := util.NewID()
if err := nl.Action(ctx, types.FredDataCategory, "", "create", newID, newUDT, nil); err != nil {
return err
}
if err := nl.Checkpoint(ctx); err != nil {
return err
}
return nl.LinkFrom(ctx,
parentID, types.FredDataCategory,
newID, types.FredDataCategory,
types.FredDataSubCategory,
"has subcategory", "parent category", 10,
)NL_CLIENT_NODE_AND_LINK_ACTION). That convenience is not currently exposed on the external Go API. External agents must use explicit create β checkpoint β link (in-context) or the equivalent multi-checkpoint command sequence (out-of-context).Long-running external work
Handlers should finish quickly (sub-second target after scheduling work). External HTTP or AI can take minutes and will trip action timeouts and NATS heartbeats if done inline.
Pattern (OpenAI-style):
- In the handler: validate, mark
InProgress, build form/image,StoreNode, enqueue work, return. - Capture before return: message header, user/graph context, node and graph ids.
- Background worker:
- call the external API
ReportNodeActivityperiodically so the UI pulses- notify the user on start/failure/success
- apply results with
ExecuteCommandSequence(new out-of-context stream)
- Never call
nl.StoreNodefrom the background after the original handler returned.
Command sequences
seq, createCP := nodlinRequest.NewCommandRequestSequence()
correlationID := shortid.MustGenerate()
actionMsg := &nodlinCommands.ExtNodeActionMsg{
NodeID: newID, NodeVersion: shortid.MustGenerate(),
NodeType: types.MyItem, Action: "create", UDT: *itemUDT,
GraphID: graphID, GraphDomain: domain,
}
actionMsg.WithCommandID(shortid.MustGenerate()).WithCorrelationID(correlationID)
actionMsg.WithMessageHeader(domainAgentId, extAgentId, remoteAgentId, hostname).
AsUser(userID, userDomain).ForGraph(graphID, domain)
createCP.AddRequest(actionMsg)
linkCP := createCP.NextCheckpoint()
linkMsg := &nodlinCommands.ExtNodeLinkMsg{
Operation: nodlinAgent.NL_LINK_CREATE,
FromNodeID: parentID,
FromNodeType: types.MyCategory,
ToNodeID: newID,
ToNodeType: types.MyItem,
OverRelation: types.RelHasItem,
UserLabel: "has item",
Weight: 10,
}
linkMsg.WithCommandID(shortid.MustGenerate()).WithCorrelationID(correlationID)
linkMsg.WithMessageHeader(domainAgentId, extAgentId, remoteAgentId, hostname).
AsUser(userID, userDomain).ForGraph(graphID, domain)
linkCP.AddRequest(linkMsg)
_ = agent.ExecuteCommandSequence(ctx, *seq, /* max duration */ nil)Checkpoint ordering: create nodes β next checkpoint β links β optional status update. Stamp every command with header, user, graph, command id, and shared correlation id.
If two sequential handlers might start the same background job for one node, guard with a module-level set/mutex.
Forms (vueform)
External Go agents build the same vueform JSON schemas the web client understands. Concepts match the forms guide; the Go builder lives in github.com/johnha/nodlinGo/v2/vueform.
form := vueform.NewForm()
view := form.Schema.AddGroup("mainView")
header := view.AddGroup("_header", vueform.SchemaOpts{Align: vueform.String("right")})
col1 := header.AddColumn("_h1", 3)
col2 := header.AddColumn("_h2", 9)
col1.AddImage("_icon", string(images.ICON), vueform.ImageOpts{Width: vueform.String("48")})
col2.AddStatic("_title", "h3", vueform.StaticOpts{Content: util.PStr("Category")})
view.AddTextInput("name", vueform.SchemaOpts{
Label: vueform.String("Name"),
Info: vueform.String("Short display name for this category"),
})
// expandable editor for long descriptions
// buttons with Action + Payload for custom actions
schemaBytes, err := form.JsonForm() // wraps as {"vueform": ...}
updated.Schema = *(*schema.NodeJsonSchema)(schemaBytes)User / group select (AddUserGroupSelect)
Same control as Starlark addUserGroupSelect / client type user-group-select. Field value is a ContactList object:
{ Users: [{userid, domain:{domainName}}], Groups: [{groupName, group_domain:{domainName}}], Domains?: [{domainName}] }.
Defaults when opts are omitted: multi-select, users + groups, domains off; default value {Users:[], Groups:[]}.
allowDomains := false
singleOnly := false
allowUserOnly := false
view.AddUserGroupSelect(
"assignees",
vueform.UserGroupSelectOpts{
SingleContactOnly: &singleOnly,
AllowUserOnly: &allowUserOnly,
AllowDomains: &allowDomains,
},
vueform.SchemaOpts{
Label: vueform.String("Assignees"),
Info: vueform.String("Select users and/or groups responsible for this work"),
},
)
// Single owner (users only)
trueVal := true
view.AddUserGroupSelect(
"owner",
vueform.UserGroupSelectOpts{SingleContactOnly: &trueVal, AllowUserOnly: &trueVal},
vueform.SchemaOpts{Label: vueform.String("Owner")},
)| Option | Meaning |
|---|---|
SingleContactOnly | One contact vs multi |
AllowUserOnly | Hide groups |
AllowDomains | Include domains in the picker |
On the next action, read the UDT field as ContactList (same JSON keys as Starlark). Platform user actions/assignments for scripts are created via the script runtime (createAction / createAssignment); Go external agents typically use command sequences / response-server messages for equivalent work. See the forms guide for Starlark end-to-end assign examples.
Rules:
- Every editable field needs
Infohelp (meaning, units, ranges). - Tabs that accept input need a unique save button id when multiple saves exist.
- Action buttons:
ButtonOpts{Action, Payload}map into UDT fields on the next action. - Prefer progressive disclosure (expand/add buttons) over dumping huge child lists as always-visible nodes.
Common builders: AddGroup, AddColumn, AddTextInput, AddTextArea, AddEditor, AddExpressionEditor, AddUserGroupSelect, AddButton, AddSlider, AddRadioGroup, AddSelect, AddCheckbox, AddStatic, AddImage, AddSpace, AddDivider, optional tabs via form.AddTab.
Node display and images
Users scan the canvas by label, icon, summary, and edge labels. Space is small β the node image should communicate status at a glance so users need not open the form.
Always set on store:
LabelSummary(markdown: description and key metrics; not only a repeat of the label)NodeSizeX/NodeSizeY(common ~250Γ150β350Γ200)- Runtime
Image - Type-level
Iconand markdownHelp
Prefer templated SVG for bespoke cards
| Approach | When |
|---|---|
SVG template + image.NewNodlinSVGImage | Status colours, multi-line text, designer-owned layout |
PNG via nodlinImage text box | Simple bordered label + icon chrome |
const cardSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="160">
<rect width="100%" height="100%" fill="{$BG}" rx="12"/>
<text x="16" y="40" font-size="18" fill="#fff">{$LABEL}</text>
<text x="16" y="70" font-size="14" fill="#eee">{$STATUS}</text>
</svg>`
svg := strings.NewReplacer(
"{$BG}", bg,
"{$LABEL}", html.EscapeString(label),
"{$STATUS}", status,
).Replace(cardSVG)
node.Image = image.NewNodlinSVGImage(svg)Keep templates as complete SVG documents with placeholders β not ad-hoc concatenation of path fragments β so designers can round-trip through tools such as BoxySVG. Encode scannable signals (status colour, type mark, short label, counts).
Design checklist
Before coding types, answer:
- What decisions does the user make on the canvas vs in a form?
- What must recompute when something else changes? (Only those get reactive edges.)
- Can collaboration be assignments/FYIs instead of new nodes/fields?
- Can an internal workflow type cover this instead of a new type?
- Can two concepts be fields on one node without losing search/link value?
- Are edges labeled in language the user understands?
- Is fan-out bounded, or do we need expand-on-demand actions?
- If there is a cycle, will handlers converge at quiescence?
Implementation checklist
- Agent name, version, ExecUser/ExecDomain set
- Each type has icon, label, markdown Help, create, delete
- Considered assignments/FYIs and internal workflow types before new types
- Constants for all type/relation FQNs
- Forms with field Info, header icon, unique save/action button ids
- Labels, summaries, scannable images set on store
- Event subscriptions match true dependency edges only
- InverseRelation only where reverse reactivity is required
- create β checkpoint β link (or command-sequence equivalent)
- Long I/O offloaded; command sequences for out-of-context writes
- Activity reporting + user notifications for multi-second jobs
- Secrets from env/flags
- Agent builds and registers cleanly in your environment
Common failure modes
| Symptom | Likely cause |
|---|---|
| Action timeout | External I/O awaited inside the handler |
| Context not found on store | Background used nl.StoreNode after handler return |
| Edge shows FQN | Missing UserLabel on link |
| Dependent never updates | Wrong edge direction, missing event, or missing InverseRelation |
| Stale derived fields after save | create did not recompute like the event handler |
| Agent dies under load | Blocking work without returning; heartbeat starvation |
| Registration fails | Missing hostname / NATS / domain agent id, or invalid spec |
Example agents
| Example | What to copy |
|---|---|
| FRED | Clean scaffold, REST expand-on-demand, forms with action payloads, simple images |
| OpenAI | Background workers, ReportNodeActivity, multi-node command sequences, named reactive events |
Study those implementations alongside this guide when building a new agent.
Package map (quick)
| Import path | Role |
|---|---|
github.com/johnha/nodlinGo/v2/agent | NewNodlinAgent, command sequences, activity, notifications |
github.com/johnha/nodlinGo/v2/nodlinAgent | Specs + Nodlin handler context |
github.com/johnha/nodlinGo/v2/nodlinTypes | Wire types, user/graph context |
github.com/johnha/nodlinGo/v2/nodlinCommands | Out-of-context command messages |
github.com/johnha/nodlinGo/v2/nodlinRequest | Command sequences and checkpoints |
github.com/johnha/nodlinGo/v2/node/udt | UDT encode/decode |
github.com/johnha/nodlinGo/v2/vueform | Form builder |
github.com/johnha/nodlinGo/v2/image | PNG/SVG node images |
github.com/johnha/nodlinGo/v2/util | IDs and pointer helpers |
github.com/johnha/nodlinImage/... | Text-box icon images (optional) |
Summary
External Go agents let Nodlin reach the outside world without abandoning the reactive graph model. Register types, react with focused handlers, store scannable nodes, and keep long work out of the request path. Prefer platform assignments, FYIs, and workflow types before inventing parallel concepts β and treat every relation as a deliberate dependency, not decoration.