Appearance
Your components depend on each other. A handler calls a service; a service uses a logger. Sometimes a rule has to follow that dependency across a component boundary — the called code must obey a constraint the caller relies on. And sometimes a rule belongs to a whole business process, not a single component.
This page covers the three tools for those cases: relations (typed dependencies), ports (carry a rule across a boundary), and flows (a rule that spans a process). For the components themselves see Nodes; for the rules see Aspects.
Relations
A relation keeps your dependencies inside the shape you designed. You declare what each component is allowed to depend on, and the graph holds every component to it. That declaration is a relation: a dependency from one node to another, written in the depending node's yg-node.yaml:
yaml
# orders/order-service/yg-node.yaml
relations:
- target: payments/payment-service
type: calls
- target: shared/logger
type: usesThere are six relation types, in two families:
- Structural —
calls,uses,extends,implements - Event-based —
emits,listens
The architecture file constrains which types may target which. Each node type declares the relation types it is allowed to use and the target node types each may reach; yg check rejects a relation that breaks those rules. So if you decide a service may only call other services and use libraries, the graph holds every service to that.
Event relations come in pairs. If A emits to B, then B must declare a listens from A. yg check enforces the pairing.
Relations earn their keep two ways: yg impact uses them to compute the blast radius of a change, and the architecture allow-list keeps dependencies inside the shape you designed.
Declared relations must match real dependencies
The graph's relations only help if they match reality. Yggdrasil keeps them honest with one built-in check.
On every yg check, it parses your actual source — TypeScript/JavaScript/TSX, Python, Go, Java, PHP, Kotlin, Rust, C, C++, C#, and Ruby — and finds where one component depends on another component's code. If that dependency is not declared as a relation, it refuses the component. The issue code is relation-undeclared-dependency.
The benefit is a map you can trust. Blast-radius analysis and the architecture allow-list mean nothing if the code quietly depends on things the graph never mentions. This check closes that gap.
Two properties keep it free of false alarms:
- One-directional. A real code dependency must be declared. The reverse is not required: a declared relation needs no code behind it. Dependencies over HTTP, dependency injection, reflection, and events are legitimately declared without any resolvable call in the source, and the check never complains about a relation with no matching code.
- Mapped-target-only and unambiguous-only. It fires only when the depended-on file is mapped to a known node — a dependency on an unmapped file is a coverage matter, not a relation error. And it resolves only dependencies it can pin to exactly one target. Anything dynamic, reflective, external, or not uniquely resolvable is left alone.
This is not an aspect. It has no rule file, it is not attached to your nodes, and the draft/advisory/enforced levels do not apply — it is always an error and always blocks yg check, like the architecture and mapping validators. It cannot be suppressed.
There are two ways to clear a refusal:
- Declare the relation in the component's
yg-node.yaml, with a type the architecture allows between the two node types. - Remove the dependency if the code should not depend on the other component.
If no relation type is allowed between the two node types, that is an architecture decision. Your agent surfaces it for your confirmation — you either change a node's type so an allowed relation exists, or extend the allowed relations in yg-architecture.yaml.
One detail worth knowing: this check runs on every yg check, not only yg check --approve. It is recomputed live — parse, resolve, verify — on every run and never cached, so it is always the current truth of your code against the graph, at zero LLM cost. That is what lets a keyless CI yg check catch an undeclared dependency even though it makes no LLM calls. When adopting Yggdrasil on an existing codebase, the first run names every file, target, and the exact relations: stanza to add.
Ports
A relation connects two nodes. It does not carry the target's rules to the caller. Most of the time that is correct — calling a service does not make the service's internal rules your problem.
But sometimes it should. When the target enforces a rule that consumers must also satisfy — a correlation ID that has to flow through the call, an idempotency key, an audit trail — you model it as a port.
A port is a named entry point on a node with required aspects:
yaml
# payments/payment-service/yg-node.yaml
ports:
charge:
description: "Charge a payment method"
aspects: [correlation-tracking]A consumer opts into the port through its relation, with consumes:
yaml
# orders/order-service/yg-node.yaml
relations:
- target: payments/payment-service
type: calls
consumes: [charge]Now orders/order-service must satisfy correlation-tracking for its own code, because it consumes the charge port. The rule has crossed the boundary.
Why this exists. A rule attached to a parent node reaches all of its children automatically. But it does not cross a relation. A helper that lives outside the audited parent, yet gets called from inside it, would slip past the audit rule. Ports restore the boundary: the owner publishes the rule as a port, the caller declares consumes, and the rule reaches the caller's code along the call.
If the target declares ports and the consumer's relation does not declare consumes, yg check fails with a blocking error (code port-missing-consumes). It names the relation, explains that the target's port rules will go unverified without a consumes, and tells you to add it. There is no "accept the gap" option: declare which ports you consume, or remove the ports from the target.
Flows
A flow is a business process that spans several components — "customer places an order, payment is captured, inventory is reserved." It groups the participating nodes and attaches shared rules to all of them.
yaml
# .yggdrasil/flows/checkout/yg-flow.yaml
name: Checkout
description: "Customer places order, payment is processed, inventory reserved"
nodes:
- orders/order-service
- payments/payment-service
- inventory/inventory-service
aspects:
- correlation-trackingEvery aspect on the flow applies to every participant. So correlation-tracking above is now a rule each of those three services must satisfy — one place to require it across a whole process, instead of repeating it on every node.
Declaring a parent node as a participant includes all of its descendants. List orders and every node under it joins the flow; add a new child later and it is already covered, no edit to the flow file.
A flow is not a call chain. It describes the why — the business process being served — while relations describe the how, what calls what. Both can exist between the same nodes at once. Use a flow when a real-world process spans multiple components and a shared rule applies across them; if you only need to apply a rule to a subset of participants, an aspect can carry a when predicate per attach site.