The extension ladder
Yano’s core provides ordering, threshold finality, deterministic state, proofs, anchoring, effects, plugin lifecycle, health, and metrics. Application teams extend the application layer, not the consensus runtime.
The plugin framework exists so that a domain can evolve without anyone forking Yano. Its central rule is simple:
Choose the smallest extension that models your outcome.
What you're seeing: start with the smallest extension that models your outcome. Each answer says what joins chain identity and how you change it later.
What do you need to change?
- What the ledger records, and how commands change it
Does a stock machine or recipe already model the outcome?
- YesConfiguration only
Select a stock machine or recipe, identically on every member. No JAR, no build.
- Code
- None
- Joins chain identity
- The selected machine or profile and its committed settings
- To change it later
- A fresh chain, or a governed activation where the setting allows one
- No
Do existing machines already have the commands and events you need?
For example: when a registry put happens, append an audit entry; when an approval passes, apply a map write.
- Yes, I need to connect them
Can bindings express the coordination?
Bindings map typed event fields to commands, check conditions and lookups, and attach forbid-only admission rules, with restricted CEL. They cannot call Java, query the network, loop, or create authority.
- YesDeclarative bindings
The bundled
declarative-compositemachine connects existing machines. You write YAML; the tooling compiles it to canonical IR that every member executes.- Code
- None: YAML compiled to IR
- Joins chain identity
- The IR, component settings and limits, in the composite profile digest
- To change it later
- A new workflow generation by governed activation; preflight with
bindings profile-check
- No, it needs coordination outside that languageA Java composite plugin
A small provider that arranges existing components: their order, topics, quotas and workflow transitions. It is short, but it is consensus code.
- Code
- A small, reviewed Java provider
- Joins chain identity
- The profile id and digest; component order is committed profile data
- To change it later
- Stage the reviewed bundle on every member, then a governed activation
- Yes
- No, I need new state or new transition rulesA state-machine plugin
Implement
AppStateMachineand contribute it throughAppStateMachineProvider. Yourapplyruns on every member and must be deterministic.- Code
- A codec, admission checks and a deterministic
apply - Joins chain identity
- The machine id and committed settings; every member runs the same bundle
- To change it later
- A governed activation, or a new id and namespace; never member by member
- Yes, I need to connect them
- Yes
- Something around the ledger: actions, block streams, Cardano facts, read APIs or keys
Which of these?
- Perform an external action after a decision is finalAn effect executor
Performs an authorized external action after the effect’s finality gate, outside
apply. With result policyCHAIN, the outcome comes back as a member-signed~fx/resultmessage.- Contribution
AppEffectExecutorFactory, trust tierPRIVILEGED_LOCAL- Reaches state
- Only through the ordered
~fx/resultinput - Configuration
- Endpoints and secrets stay node-local
- Deliver finalized blocks to another systemA finalized-stream sink
Receives finalized blocks and delivers them to another system, such as Kafka or a webhook. Nothing flows back into the ledger.
- Contribution
FinalizedStreamSinkFactory, trust tierAUXILIARY_LOCAL- Reaches state
- No
- Bring Cardano deposits or metadata into the ledgerAn L1 observer
Watches Cardano for deposits or metadata labels. Observations enter as
~l1/*messages that every member re-derives from its own L1 view before it votes.- Contribution
L1ObserverProvider, trust tierCONSENSUS- Reaches state
- Yes, as ordered, certified inputs
- Joins chain identity
- Observer profiles are committed in the consensus-context digest
- Change how proposers are chosenA sequencer mode
Decides who may propose at each height and whose proposals are acceptable. It cannot weaken finality: certificates and vote locks are enforced by the framework.
- Contribution
SequencerModeProvider, trust tierCONSENSUS- Reaches state
- No, but every member must run the same mode
- Serve a read API, or keep member keys outside the nodeA domain API or a signer
A domain API serves bounded reads over committed state; keep it read-only unless commands still enter through authenticated submission. A signer keeps the member key in external custody.
- Contribution
DomainApiProviderorSignerProviderFactory, trust tierPRIVILEGED_LOCAL- Reaches state
- No
- Perform an external action after a decision is final
What do you need to change?
Does a stock machine or recipe already model the outcome?
Do existing machines already have the commands and events you need?
For example: when a registry put happens, append an audit entry; when an approval passes, apply a map write.
Can bindings express the coordination?
Bindings map typed event fields to commands, check conditions and lookups, and attach forbid-only admission rules, with restricted CEL. They cannot call Java, query the network, loop, or create authority.
Which of these?
Select a stock machine or recipe, identically on every member. No JAR, no build.
- Code
- None
- Joins chain identity
- The selected machine or profile and its committed settings
- To change it later
- A fresh chain, or a governed activation where the setting allows one
The bundled declarative-composite machine connects existing machines. You write YAML; the tooling compiles it to canonical IR that every member executes.
- Code
- None: YAML compiled to IR
- Joins chain identity
- The IR, component settings and limits, in the composite profile digest
- To change it later
- A new workflow generation by governed activation; preflight with
bindings profile-check
A small provider that arranges existing components: their order, topics, quotas and workflow transitions. It is short, but it is consensus code.
- Code
- A small, reviewed Java provider
- Joins chain identity
- The profile id and digest; component order is committed profile data
- To change it later
- Stage the reviewed bundle on every member, then a governed activation
Implement AppStateMachine and contribute it through AppStateMachineProvider. Your apply runs on every member and must be deterministic.
- Code
- A codec, admission checks and a deterministic
apply - Joins chain identity
- The machine id and committed settings; every member runs the same bundle
- To change it later
- A governed activation, or a new id and namespace; never member by member
Performs an authorized external action after the effect’s finality gate, outside apply. With result policy CHAIN, the outcome comes back as a member-signed ~fx/result message.
- Contribution
AppEffectExecutorFactory, trust tierPRIVILEGED_LOCAL- Reaches state
- Only through the ordered
~fx/resultinput - Configuration
- Endpoints and secrets stay node-local
Receives finalized blocks and delivers them to another system, such as Kafka or a webhook. Nothing flows back into the ledger.
- Contribution
FinalizedStreamSinkFactory, trust tierAUXILIARY_LOCAL- Reaches state
- No
Watches Cardano for deposits or metadata labels. Observations enter as ~l1/* messages that every member re-derives from its own L1 view before it votes.
- Contribution
L1ObserverProvider, trust tierCONSENSUS- Reaches state
- Yes, as ordered, certified inputs
- Joins chain identity
- Observer profiles are committed in the consensus-context digest
Decides who may propose at each height and whose proposals are acceptable. It cannot weaken finality: certificates and vote locks are enforced by the framework.
- Contribution
SequencerModeProvider, trust tierCONSENSUS- Reaches state
- No, but every member must run the same mode
A domain API serves bounded reads over committed state; keep it read-only unless commands still enter through authenticated submission. A signer keeps the member key in external custody.
- Contribution
DomainApiProviderorSignerProviderFactory, trust tierPRIVILEGED_LOCAL- Reaches state
- No
Other contribution kinds work around the ledger rather than inside apply():
effect executors, finalized-stream sinks, domain APIs, signers, sequencer modes
and L1 observers. Their I/O runs outside apply(). Their outputs reach state only
as ordered, certified inputs; see around the ledger.
Rung 0 — configuration only
Section titled “Rung 0 — configuration only”Select one stock machine or profile, identically on every member:
yano.app-chain.state-machine: kv-registryNo JAR, no build, no signing. This covers ordered-log, kv-registry,
authenticated-map, approvals, balances, doc-trail, role-approvals,
and the evidence profiles. See the recipe catalog.
For stock composite and role profiles, the profile identifier and its configuration digest become part of chain identity — so select them for a fresh chain, or through a governed activation.
Rung 1 — declarative bindings
Section titled “Rung 1 — declarative bindings”Use this when existing machines already expose the commands and events you
need, and you only need to connect them: when this component emits this event,
check these conditions and send that command. The bundled
declarative-composite machine compiles a YAML document into canonical IR that
every member executes identically. Admission rules in the same document can
forbid commands, for example over a limit or from the wrong organization.
No Java is involved. The compiled IR, component settings and limits are part of the committed composite profile, so changing a deployed workflow is a governed profile change, not a file edit. Bindings cannot call Java, query the network, or create authority. Start with the bindings learning path.
Rung 2 — a small Java composite plugin
Section titled “Rung 2 — a small Java composite plugin”Use this when every component you need already exists, but the coordination is outside what bindings can express. A composite explicitly defines:
- component ids and versions;
- deterministic application order;
- routed public topics;
- per-component quotas;
- workflow transitions between components; and
- one committed profile identity and digest.
The Java class is intentionally small, and intentionally consensus-critical. Component order is committed profile data: in a Java composite it is this code, and in a declarative composite it is the compiled IR. Members that ran a different order would derive different roots, so every member must run the same reviewed bundle, and a change goes through governed activation.
The effect-gated evidence and role-evidence presets are the reference implementations. Package the provider, manifest, service entry, and components in one reviewed bundle.
Rung 3 — a custom state-machine plugin
Section titled “Rung 3 — a custom state-machine plugin”Only when you need genuinely new state or new rules.
import com.bloxbean.cardano.yaci.core.protocol.appmsg.model.AppMessage;import org.yanoproject.api.appchain.AppBlockExecutionContext;import org.yanoproject.api.appchain.AppStateMachine;import org.yanoproject.api.appchain.AppStateWriter;import org.yanoproject.api.appchain.effects.AppEffectEmitter;
public final class ShipmentStateMachine implements AppStateMachine { @Override public String id() { return "shipment-v1"; }
@Override public AdmissionResult validate(AppMessage message) { // Bounded structural validation only; never perform I/O here. return ShipmentCodec.isWellFormed(message.getBody()) ? AdmissionResult.accept() : AdmissionResult.reject("INVALID_SHIPMENT_COMMAND"); }
@Override public void apply(AppBlockExecutionContext context, AppStateWriter state, AppEffectEmitter effects) { for (AppMessage message : context.messages()) { // Deterministic, bounded transitions only. Bytes you cannot decode are a no-op. } }}ShipmentCodec stands for your own command codec. Reject with a public code of
at most 32 uppercase letters or underscores; REST callers see any other reason
text as APPLICATION_REJECTED. Contribute the
machine through AppStateMachineProvider, add the service entry and the plugin
manifest, then copy the bundle JAR into the configured plugin directory on every
member. Yano itself is never recompiled for a JVM deployment.
Read Determinism rules before writing apply(),
and Consensus rules before changing one that is
already live.
Around the ledger
Section titled “Around the ledger”These contributions do their work outside apply(). That does not make them
irrelevant to the state root: whatever they bring back enters only as ordered,
certified input that every member checks.
| Contribution | Purpose | How it reaches state |
|---|---|---|
| Effect executor | Perform an authorized external action after its finality gate. | With result policy CHAIN, the outcome returns as a member-signed ~fx/result message. |
| L1 observer | React to Cardano address deposits or metadata labels. | As ~l1/* messages that every member re-derives from its own L1 view; observer profiles are committed in the consensus-context digest. |
| Sequencer mode | Choose who may propose at each height. | It does not; every member must run the same mode, and it cannot weaken finality. |
| Finalized-stream sink | Deliver finalized blocks to Kafka, a webhook, or your own system. | It does not. |
| Domain API and committed queries | Bounded read surfaces over your own state. | It does not. Keep it read-only unless writes still enter through authenticated submission. |
| Signer | External key custody or a KMS. | It does not. |
The host derives a trust tier from each contribution kind: state machines,
sequencer modes and L1 observers are CONSENSUS; executors, signers and domain
APIs are PRIVILEGED_LOCAL; sinks, health and metrics are AUXILIARY_LOCAL.
Create a custom executor when the action needs typed target aliases, authentication, polling, reconciliation, or a domain-specific receipt. Keep endpoints and secrets in node-local executor configuration, never in replicated effect payloads.
What is and is not a plugin
Section titled “What is and is not a plugin”Every optional behavior a running node can independently select or manage
crosses the plugin catalog boundary — stock state machines, capabilities,
connectors, effects, observers, indexers, and product runtime behavior. All of
it activates through PluginProviderRegistry and a schema-v1 plugin manifest.
There is no raw ServiceLoader path, no direct host construction, no product
switch, and no product-specific host CDI or REST activation.
Pure libraries, DTOs, clients, codecs, testkits, CLIs, on-chain validators, and deterministic helpers are ordinary JARs. They only become plugins if the host selects or manages them.
Runtime plugins additionally must:
- publish dependency-complete bundles;
- not embed host SPI classes;
- declare a compatible
yanoApirange:minandmaxAPI majors and aminLevel(there is no maximum level); and - have bounded lifecycle cleanup.
The rest of this section
Section titled “The rest of this section”- Scaffold, sign, install — the actual lifecycle, command by command.
- SPI and manifest — the three bounded contracts your JAR carries and the trust envelope over them.
- How plugins load — what tools check before deployment, what the node checks at start-up, and what neither checks.
- Consensus rules — what plugin code may do, and how to evolve it without forking a live chain.
- Testing and deployment — the testing ladder and operational expectations.
The hands-on version of all of this is Tutorial 8.