Skip to content

Ikanos — Schema — Aggregates

A domain aggregate, in the DDD sense, groups reusable flows — transport-neutral invocable units that adapters (MCP tools, REST operations) reference via ref:. Aggregates factor domain behavior so it is defined once and reused across REST, MCP, Skill, and future adapters without duplication.

Introduced in schema v1.0.0-alpha1. Refactored in v1.0.0-alpha3 to use keyed maps (aggregates: {ns: ...}, flows: {name: ...}) and rename functions:flows:.


Why aggregates

Without aggregates you copy the same steps: block across the MCP tool, the Skill catalog, and the REST operation that all want to expose the same domain operation. That's where capabilities go to die.

With aggregates:

  • Logic lives once, at capability level.
  • Adapters declare ref: {aggregate-namespace}.{flow-name} and inherit the chain.
  • Adapter-local explicit fields override inherited ones.
  • semantics are derived into MCP hints automatically.

Aggregate object

capability:
  aggregates:                           # keyed map; key IS the namespace
    forecast:
      display: "Forecast"
      flows: { ... }

When used as a standalone document (root-level aggregates, no capability:) the shape is a list, with namespace declared per entry:

ikanos: "1.0.0-alpha3"
aggregates:
  - display: "Forecast"
    namespace: "forecast"
    flows: { ... }
Field Required Description
display Human-readable name
namespace ✅ (root form only) IdentifierKebab; used as the first segment in ref:. Under capability.aggregates the map key IS the namespace.
flows At least one flow

Rules

  • namespace MUST be unique across all aggregates.
  • No additional properties.

AggregateFlow

flows:                                  # keyed map; key IS the flow name
  get-forecast:
    description: "Fetch current weather forecast for a location."
    semantics:
      safe: true
      idempotent: true
    inputParameters:
      location: { type: string, description: "City or coordinates" }
    call: "weather-api.get-forecast"
    with: { location: location }
    outputParameters:
      - { type: string, mapping: "$.forecast" }
Field Required Description
(map key) IdentifierKebab, unique within the aggregate — IS the flow name
description Inherited by adapter units that omit their own
semantics Transport-neutral behavior — see below
inputParameters Flow-level input parameters (keyed map)
call + with Simple mode
steps + mappings Orchestrated mode
outputParameters Flow-level output parameters

Rules

  • Exactly one mode (call: or steps:).
  • Flow names (map keys) MUST be unique within an aggregate.
  • No chained refs — a flow cannot itself use ref:.

Semantics

semantics:
  safe: true
  idempotent: true
  cacheable: true
Field Default Meaning
safe false If true, the flow does not modify state
idempotent false If true, repeating the call has no additional effect
cacheable false If true, the result can be cached

All optional. Omitted fields fall back to their defaults.


Semantics → MCP hints derivation

When an MCP tool references an aggregate flow via ref:, the flow's semantics are automatically derived into MCP hints:

semantics Derived MCP hints Rationale
safe: true readOnly: true, destructive: false A safe flow doesn't change state
safe: false (or absent) readOnly: false Default — may have side effects
idempotent: true idempotent: true Direct 1:1 mapping
cacheable (not mapped) No MCP equivalent; informational for future adapters

Override rule: Each non-null field in the tool-level hints wins over the derived value. Absent tool-level fields still inherit from semantics.


Using ref from adapters

exposes:
  - type: mcp
    namespace: forecast-mcp
    tools:                              # keyed map; key IS the tool name
      get-forecast:
        ref: forecast.get-forecast      # minimal — everything inherited
      weather:
        ref: forecast.get-forecast      # override only the name (via map key)
      weather-open:
        ref: forecast.get-forecast
        hints:
          openWorld: true               # add MCP-specific hint; semantics still derive the rest

  - type: rest
    namespace: forecast-rest
    resources:                          # keyed map
      forecast:
        path: "/forecast/{location}"
        operations:                     # keyed map
          get-forecast:
            ref: forecast.get-forecast
            method: GET
            inputParameters:
              location: { in: path, type: string }

The merge rules:

  • Explicit fields on the tool/operation override inherited fields.
  • Fields not set on the tool/operation are inherited from the flow.
  • description: is optional when using ref: — it defaults to the flow's value. The tool/operation name is taken from the map key.

When to introduce an aggregate

Symptom Solution
Same steps: block appears in two tools Move into an aggregate flow
MCP tool and REST operation share logic Reference the same aggregate from both
Manual hints: { readOnly: true } repeated Move semantics to the aggregate and let derivation do the work
Logic spans more than five steps Aggregate it; the spec stays scannable