Declare nodes
Nodes are the things in your architecture — services, databases, actors, networks.
Every CALM node lives in a *.calm.json file under architecture/
and (if it represents code in your repo) is bound to a directory by a
mapping entry in .archrails/config.yml. This lesson covers
the node catalog, the required fields, and the path-aligned ID convention that makes
the binding work.
Anatomy of a node
A node is an object inside the top-level nodes array of any CALM file.
The minimum-viable node looks like this:
{
"unique-id": "orders-server",
"node-type": "service",
"name": "Orders Server",
"description": "Accepts new orders from customer-portal and persists them to orders-db."
}
Four fields, four jobs: unique-id is the stable handle every other CALM
object (relationships, flows, controls) uses to refer to this node;
node-type tells the validator what kind of thing it is;
name is the human-readable label shown in dashboards;
description is your one-sentence answer to "what is this and why does it exist?"
The node-type catalog
CALM 1.0 ships a small fixed catalog of node types. Pick the most specific one
that fits; if nothing fits cleanly, use system and revisit later.
service A running process you ship. Most code nodes are services. database Any persistent data store — Postgres, Mongo, Redis, DynamoDB, S3 bucket used as a store. network A network boundary — VPC, subnet, k8s cluster, region. Other nodes aredeployed-inthese. actor A human role — customer, support agent, compliance officer. Actors onlyinteractwith other nodes. system A coarse grouping — "the trading platform", "the CRM". Use sparingly; prefer breaking down into services. ldap A directory service — Active Directory, Okta, Auth0. data-asset A logical data set (not a store) — "customer PII", "settlement records". Useful for data-lineage flows. webclient A browser-resident UI — SPA, mobile web app.
service? ArchRails
enforces different rules per type. A database can't talk to another
database (only services can connect to databases). An actor
is barred from connects relationships (they interact with
systems instead). Picking the right type is how you get the right validation for free.
Required fields
The CALM v1 schema requires four fields on every node:
-
unique-id— kebab-case, unique within the CALM file. Should match the directory name (see next section). -
node-type— one of the values from the catalog above. -
name— short human label, title-case is the convention. -
description— one sentence. What is this and why does it exist? The dashboard surfaces this on hover.
Optional fields you'll reach for next: interfaces (host/port specs),
controls (compliance attachments — see
lesson 5), and repository
(only when this node lives in a different repo — see
lesson 3).
Why IDs are path-aligned
ArchRails binds CALM nodes to directories via the mapping block in
.archrails/config.yml. If your node's unique-id matches
its top-level directory name, the binding is trivial and bootstrap inference catches
it with high confidence. If they diverge, you have to write the mapping by hand and
re-read it every time you onboard a new teammate.
Recommended:
# directory # node unique-id # confidence customer-portal/ customer-portal high ✓ orders-server/ orders-server high ✓ services/payment-api/ payment-api high ✓
Avoid:
# directory # node unique-id # confidence customer-portal/ cust-front REVIEW REQUIRED ✗ backend-v2/ orders-service REVIEW REQUIRED ✗ src/ payment-api conflict ✗
The first two confuse bootstrap inference; the third is the worst case — a single
src/ shared by three services means none of them can claim it without
misattributing the others' files during PR validation.
Mapping the node to a directory
The CALM file says what exists; .archrails/config.yml says
which files belong to which node. After declaring a node in CALM, add a
mapping entry:
mapping: - path: "orders-server" node: orders-server - path: "services/payment-api/**" node: payment-api
Both prefix paths and glob patterns work. Use a glob when the service's code is split
across siblings (payment-api-core/, payment-api-grpc/) and
you want one node to own all of it.
database node representing a managed RDS instance has no code in your
repo — leave the mapping commented out. Same for actor and external
system nodes. Bootstrap emits these as commented placeholders;
keep them that way.
Interfaces — hosts, ports, protocols
An interface declares how something connects to this node. Most services
have one, databases have one, and the most common shape is
host-port-interface:
{
"unique-id": "orders-server",
"node-type": "service",
"name": "Orders Server",
"description": "Accepts new orders from customer-portal.",
"interfaces": [
{
"unique-id": "orders-http",
"type": "host-port-interface",
"host": "orders-server.internal",
"port": 443
}
]
}
Relationships then target a specific interface by unique-id, so when a
service exposes both an HTTP port and a gRPC port, callers pick the right one. The
full interface vocabulary (oauth2-audience,
rate-limit-interface, plus JSON-Schema-backed custom types) is in the
FINOS CALM spec — the host/port form covers ~80% of real-world cases.
Worked example · add a Redis cache
Suppose orders-server just got a Redis cache for hot order lookups, and
we want it under governance. Two edits.
1. Declare the node in architecture/system.calm.json:
{
"unique-id": "orders-cache",
"node-type": "database",
"name": "Orders Cache",
"description": "Redis cache fronting orders-db for hot lookups. TTL 5m.",
"interfaces": [
{
"unique-id": "orders-cache-redis",
"type": "host-port-interface",
"host": "orders-cache.internal",
"port": 6379
}
]
}
2. Skip the mapping — Redis is managed infrastructure with no
code in this repo. No mapping entry needed. (Compare with
orders-server, which is in this repo, so it does get a mapping.)
That's the node. To make it part of the architecture, you'll also need a
connects relationship from orders-server to the Redis
interface — covered in lesson 3.
Common mistakes
I used id instead of unique-id and validation fails.
Easy one. The CALM spec uses unique-id (kebab-case, hyphenated) on
every object — nodes, interfaces, relationships, flows. Tools like the
FINOS CALM CLI will surface this as a schema error pointing at the offending
object.
My node has no top-level directory — it's spread across src/.
Two options. Best: reorganize so each service has its own
directory under the repo root — this also makes ownership, CI, and code
review cleaner. Acceptable: use a glob mapping like
"src/orders/**" and live with the fact that your other services need
equally specific globs to avoid overlap.
I picked system but the dashboard says “no validation rules apply.”
system is a deliberately loose type — CALM doesn't know enough
about it to enforce much. If the thing you're modeling is actually a service or
a database, change the node-type and rerun validation. Use
system only for genuine coarse groupings like "the trading platform"
that contain other nodes.
Bootstrap put a REVIEW REQUIRED comment in my mapping block. What now?
Bootstrap couldn't match the node to a directory. Three resolutions, in order of how often they apply:
- Infrastructure or external: the node has no code in this repo (RDS, S3, third-party API, human actor). Leave the comment block in place as documentation that the node was considered and intentionally left unmapped.
- Misnamed: the directory and the node ID don't align. Rename the node ID to match the directory (preferred) or add the mapping by hand.
- Genuinely missing: the node was declared but the code hasn't been written yet. Same fix — mapping by hand — or remove the node from CALM until the code exists.