Component Links
Most components in a Live System are not isolated. A workload needs to reach a database. A Spark cluster needs credentials for an object store. A workload needs to be exposed through an API gateway. These are runtime relationships, and Fractal Cloud models them as a first-class concept distinct from parameters and dependencies.
This page explains how the link system works and what custom-agent authors need to know to participate in it.
Parameter, Dependency, Link — three different things
It is worth being precise about the distinction, because the wrong choice produces real bugs (ordering deadlocks, missing wiring, configuration drift).
| Concept | Carries settings | What it expresses |
|---|---|---|
| Parameter | n/a — scalar values only | What the component is. CIDR block, image, name, size. |
| Dependency | no | "I cannot exist without this." Lifecycle ordering. |
| Link | yes (optional) | "I have a runtime relationship with this." Wiring. |
A Subnet depends on a VPC — it cannot be created until the VPC is. A workload links to a database — it needs the database's connection string, but the link is wired in once both sides exist. Mixing them up produces hard-to-diagnose problems.
What a link looks like
Every link is { target, settings }. The target is a reference to another component. The settings are an optional, flat map of string-keyed values.
A workload-to-workload traffic rule:
producer.linkToWorkload([
{ target: dashboard, fromPort: 8080, protocol: 'tcp' },
]);
A workload-to-API-gateway exposure:
dashboard.linkToApiGateway([
{
target: edgeStack,
prefix: '/api/dashboard',
hostname: 'crisiswatch.demo',
port: 8080,
},
]);
The SDK exposes a typed builder method per link type (linkToXxx({ ... })) — that is the developer-facing API. Under the hood, both calls produce the same { target, settings } shape.
How links are resolved
Reconciliation handles a single component at a time. Links cross component boundaries, so they cannot be resolved during a single component's reconcile pass. Fractal Cloud uses a separate, two-phase resolution framework.
For every link, there are two halves:
- Inbound — runs when the target is reconciled. Its job is to provision access for the source: create a database role, generate an object-store key, write a Mapping CR, open a security-group ingress rule. It publishes the result to the target's
outputFields. - Outbound — runs when the source is reconciled. Its job is to read what the inbound side published on the target, and configure the source: write the connection string into the source's
outputFieldsso its component handler can pick it up.
The two halves never call each other. They communicate exclusively through the control plane's outputFields for each component. This is what makes the system safe across cloud boundaries: the source agent can run in AWS while the target agent runs on-premises, and they only ever see each other's published state on the control plane.
Each half is idempotent and retried. If the target hasn't yet published its credentials when the source's outbound runs, the outbound handler returns silently and the resolver retries on the next cycle.
What a custom agent needs to do
If you are building an agent that introduces a new component type — a proprietary database, a new gateway, a new identity provider — and that component will be on either end of a link, you participate in the framework.
1. Pick which end(s) you handle
A component can be a target (someone links to it), a source (it links to others), or both. For each (sourceType, targetType) pair where your component appears, decide which sides your agent owns.
- Target-only: register an inbound handler.
- Source-only: register an outbound handler.
- Both: register both.
You do not need to handle every pair — only the ones your agent provisions. Other agents register their own halves.
2. Define the link settings contract
For each pair, document:
- Required keys. Validated at resolution time; missing keys produce a clear error.
- Optional keys with defaults.
- Semantics for missing target state. (Should the handler retry next cycle, or fail outright?)
Settings keys are part of your component's public contract. Once shipped, renaming them is a breaking change for every agent that participates.
3. Implement the handlers
Both halves are small. They check the current state, do their work if needed, and publish to outputFields using a stable naming convention.
Inbound handler — target side, called when the target reconciles:
1. Has linkedCredentials.{sourceId}.secretName already been published?
Yes → return (idempotent).
2. Provision per-source access (DB role, object-store key, Mapping CR, etc.).
3. Write credentials/endpoint to a Kubernetes Secret (or equivalent).
4. Publish to target outputFields:
linkedCredentials.{sourceId}.secretName
linkedCredentials.{sourceId}.endpoint
linkedCredentials.{sourceId}.<other keys>
Outbound handler — source side, called when the source reconciles:
1. Read linkedCredentials.{sourceId}.* from the target's outputFields.
Absent? → return silently; the inbound side hasn't run yet.
2. Read settings (e.g. bucket name, schema name) from the link.
3. Publish to source outputFields:
{linkType}.{targetId}.secretName
{linkType}.{targetId}.endpoint
{linkType}.{targetId}.<other keys>
The source's component handler reads these {linkType}.* keys when constructing pod env, Spark conf, application config — whatever it needs to make the wiring real.
4. Use the output-field naming convention
The convention is what makes cross-agent communication possible. Every existing handler follows it; your handlers must as well.
| Side | Pattern | Owner |
|---|---|---|
| Target writes | linkedCredentials.{sourceId}.{key} | inbound handler |
| Source writes | {linkType}.{targetId}.{key} | outbound handler |
{linkType} is a short kebab tag chosen by your handler — storageLink, databaseLink, apiGatewayLink, etc. Stick to one per link type and document it.
5. Register and document
Register your handler pairs at agent startup. Add a row to the Links section of your component's reference documentation describing the settings keys, defaults, and semantics. The shared cross-project link contract lives in the platform-wide reference and is the canonical place for this.
Cross-cloud links
Source and target can live in different clouds, owned by different agents. The framework supports this natively. The resolver detects when source and target advertise different provider values and surfaces it via a isCrossCloud() helper your handler can use to fall back from IAM-based auth to credential-based auth.
In practice, the framework operates the same way whether the link is intra-cloud or cross-cloud — only the auth strategy your handler chooses changes.
Anti-patterns
A few things that look reasonable but are not.
- Putting link logic inside the component handler. A workload handler that creates an API gateway Mapping directly because it sees a link couples the two component types and re-derives the same logic for every similar pair. Use a registered link handler.
- Custom output-field key names. Publishing to
creds_for_{sourceId}instead oflinkedCredentials.{sourceId}.secretNamehides the data from cross-agent consumers. Stick to the convention. - Synchronous handler chaining. A handler must not block waiting for another to run. The next reconciliation cycle will retry. The framework is lock-free by design.
- Using a dependency when you mean a link. If A only needs B's existence (ordering), it is a dependency. If A needs to read B's URL or secret, it is a link. Mixing the two produces ordering bugs and resolution loops.
Quick reference
| What you need to do | How |
|---|---|
| Resolve a link your agent's component is the target of | Register an InboundLinkHandler for (*, YourType) |
| Resolve a link your agent's component is the source of | Register an OutboundLinkHandler for (YourType, *) |
| Validate required settings | LinkSettingsValidator.validate(settings, requiredKeys, sourceId, targetId) |
| Publish credentials for a linked source | Write linkedCredentials.{sourceId}.* to your component's outputFields |
| Consume credentials a target published for you | Read linkedCredentials.{sourceId}.* from target's outputFields, write {linkType}.{targetId}.* to your own |
| Detect cross-cloud links | context.isCrossCloud() |
If you are starting a new agent and the components it manages will participate in any link relationships, set up the link framework before adding the first handler — it is much easier than retrofitting once handlers have been written inline.