Skip to content

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.

Which extension do you need?Decision aid

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?

    • Yes
      Configuration 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.

        • Yes
          Declarative bindings

          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
        • No, it needs coordination outside that language
          A 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
      • No, I need new state or new transition rules
        A state-machine plugin

        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
  • Something around the ledger: actions, block streams, Cardano facts, read APIs or keys

    Which of these?

    • Perform an external action after a decision is final
      An effect executor

      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 tier PRIVILEGED_LOCAL
      Reaches state
      Only through the ordered ~fx/result input
      Configuration
      Endpoints and secrets stay node-local
    • Deliver finalized blocks to another system
      A 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 tier AUXILIARY_LOCAL
      Reaches state
      No
    • Bring Cardano deposits or metadata into the ledger
      An 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 tier CONSENSUS
      Reaches state
      Yes, as ordered, certified inputs
      Joins chain identity
      Observer profiles are committed in the consensus-context digest
    • Change how proposers are chosen
      A 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 tier CONSENSUS
      Reaches state
      No, but every member must run the same mode
    • Serve a read API, or keep member keys outside the node
      A 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
      DomainApiProvider or SignerProviderFactory, trust tier PRIVILEGED_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.

Select one stock machine or profile, identically on every member:

yano.app-chain.state-machine: kv-registry

No 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.

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.

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.

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.

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.

ContributionPurposeHow it reaches state
Effect executorPerform an authorized external action after its finality gate.With result policy CHAIN, the outcome returns as a member-signed ~fx/result message.
L1 observerReact 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 modeChoose who may propose at each height.It does not; every member must run the same mode, and it cannot weaken finality.
Finalized-stream sinkDeliver finalized blocks to Kafka, a webhook, or your own system.It does not.
Domain API and committed queriesBounded read surfaces over your own state.It does not. Keep it read-only unless writes still enter through authenticated submission.
SignerExternal 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.

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 yanoApi range: min and max API majors and a minLevel (there is no maximum level); and
  • have bounded lifecycle cleanup.
  1. Scaffold, sign, install — the actual lifecycle, command by command.
  2. SPI and manifest — the three bounded contracts your JAR carries and the trust envelope over them.
  3. How plugins load — what tools check before deployment, what the node checks at start-up, and what neither checks.
  4. Consensus rules — what plugin code may do, and how to evolve it without forking a live chain.
  5. Testing and deployment — the testing ladder and operational expectations.

The hands-on version of all of this is Tutorial 8.