Skip to content

Repository files navigation

Polyad

Table of contents

Polyad ballet shoes fading toward the right

Polyad (named after polyads in mathematics) is a Kubernetes operator and Python SDK for writing and orchestrating adaptive microservices. Deploy, connect and scale applications as graphs within and across clusters, enforce Cheeger bounds, and adjust topology, traffic and capacity to application demand. Compose batch jobs, persistent services and supporting resources into reusable Graphs and PolyGraphs. Define how work starts, how components communicate, how they respond to application demand and which constraints must hold as the application changes, from a workflow in one cluster to a hierarchy spanning multiple clusters.[2] The Kubernetes operator is built on Kopf, the Kubernetes Operators Framework for Python.

Get started · Documentation · Helm chart

  • Compose workloads and resources. Run finite pipelines and persistent services using Jobs, Deployments, StatefulSets or DaemonSets, with dependencies, activation policies, storage and placement.
  • Scale within graph constraints. KEDA and ReplicaGroups scale individual services, whole graphs or nested compositions. Polyad refreshes live graph state and enforces GraphPolicies, including size, shape and structural Cheeger bounds, before applying scaling changes.
  • Adapt to administrator-defined demand. Soul searching uses offered throughput or a named signal, such as queued jobs, to select approved Cheeger targets, connection layouts, traffic splits and capacity lookahead. Observe recommendations or allow bounded adaptation, including preparation before throughput falls behind. Hard rules and resource ceilings stay fixed.
  • Make connectivity explicit. Choose replica connection patterns, including custom edges, and enforce network boundaries with optional NetworkPolicy and Istio integration. Istio percentage routing divides requests among workload or graph replicas using fixed splits, demand tiers or measured spare capacity.
  • Let services participate. Through the standalone Python SDK, workloads can submit compositions, activate work, request TTL-bound connections and discover permitted services across the atlas, with filtered event hooks and peer-approved connections. This application model is Service Symbiosis: services with different roles discover compatible peers and adjust their work together. Shared types and JSON Schemas are also available separately from the operator.
  • Rebalance event subscriptions. Subscribe over SSE or WebSocket and opt into paced reconnections as operator replicas change. Clients resume from their checkpoints through Service/Istio routing or discovered Pod IPs.
  • Coordinate across clusters. A root operator can run in a dedicated management cluster, deploy graphs and execution replicas into registered workload clusters, and collect their observations centrally.
  • Choose the control-plane layout. Select a single-replica or HA Helm profile. HA can run dense operators or separate gateway, executor and telemetry components managed through the operator's own Graph. Optionally persist graph state and tracked measurements in PostgreSQL.
  • Observe and coordinate changes. Inspect Prometheus and JSON metrics and OpenTelemetry traces and decision logs. The write pipeline coalesces duplicate mutations, checks dependencies before dispatch and returns stale decisions to reconciliation.

Get started

Deploy and configure

Deploy the Kubernetes operator with the Helm chart, or try the local Python scheduler. The examples cover pipelines, services, spot work, storage and nested graphs.

For application feedback, start with the approved load-profile example and its Helm reference values. The demand guide explains signal names, units and thresholds; use Observe mode to inspect recommendations before enabling adaptation.

Develop and run local demos

Applications can install the Python SDK or just the shared types without installing the operator. From a checkout, use pip install ./pkg/polyad-types; the standalone distribution is named polyad-types and exposes polyad_types.

Use Service Symbiosis to write producers and consumers that cooperate across Graphs and PolyGraphs: discover compatible peers, react to connection and capacity deltas, propagate backpressure and report useful completion. Run the standalone python demo/soul.py local demonstration to watch three service processes share queued work over an added TCP connection and roll their child workers under load. It compares completion time, latency and backlog against a fixed chain with the same load and worker limits. Run python demo/nature.py for the parent Natural Selection demonstration: changing requirements select capabilities and routes, mutate services, preserve useful survivors and retire excluded processes.

Find detailed guides

Explore graph concepts, graph policies, composition requests, the composition API, networking and event subscriptions, temporary connections, API keys and shared request lanes, or the development guide. See the documentation index for lifecycle, status, health and configuration references.

Why Polyad exists

Motivation and inspiration

While at Klaviyo, I briefly crossed paths with engineers working on a project converting the company's cloud architecture to a setup where clusters managed other clusters, a concept they'd designed at Medium. That idea helped motivate Polyad's root control plane and nested PolyGraphs. Medium's Kubernetes Infrastructure At Medium provides public background on this strategy's multi-cluster infrastructure, gradual rollouts and capacity planning.

Problems that grow with the application

Coordinating dependent services

Deploying a distributed application means deciding how its services connect, which work can run together, and how those relationships should change as demand grows. Those relationships need to remain understandable and enforceable as the application spans more services, teams and clusters.

A scaling decision that helps one service can leave the rest of its pipeline overloaded or disconnected. Teams need a way to express what must remain true for the whole application as its parts grow, shrink or span more clusters. Polyad aims to make that coordination repeatable, with room for applications to adapt within boundaries their administrators can trust.

Request latency

Growth puts pressure on both request latency and recovery time, especially when it adds interconnected dependencies and increases resource utilization. A request involving more services has more opportunities to wait on a slow component, and shared resources accumulate queues. Dean and Barroso's The Tail at Scale explains how occasional delays can become a dominant performance problem at larger scales.

Recovery and cascading failures

Recovery can also become slower and more involved. An overloaded service can shift work onto its neighbors, while retries add more demand to struggling dependencies. Restoring useful capacity may require several services to recover, new Pods or nodes to become ready, and caches to warm. Google's account of cascading failures describes how these effects can reinforce one another.

How Polyad addresses these problems

Respond close to the work

Growth also creates opportunities for parallelism and spare capacity. Dividing work into well-defined boundaries lets individual requests and recovery decisions remain local as the application expands. Polyad's architectural objective is to preserve these short reaction paths while coordinating changes that affect shared dependencies. Available capacity becomes useful when services can reach it and adapt how they use it.

Service Symbiosis brings part of that response into the microservices themselves. A producer can reduce outstanding work when a consumer slows down, or send compatible work to an authorized peer with spare capacity. A consumer can adjust concurrency, switch worker profiles or drain accepted jobs before replacing workers. The Python SDK's adaptive interface exposes connection, capacity and decision changes to application-defined strategies. These local responses let services use their existing resources while additional infrastructure is being prepared. Services can also request temporary connections and new compositions within their permissions.

Coordinate graph-wide changes

Graphs and PolyGraphs make related workloads and their connections reusable deployment units. GraphPolicies express their structural requirements, which Polyad checks against live state before applying ReplicaGroup scaling requests. A root control plane extends that model across registered clusters, coordinating deployments and collecting observations.

At those larger boundaries, Soul searching uses observed demand, completed throughput and reported spare capacity to guide approved connection changes and traffic balancing. Traffic can shift between individual services, whole graph replicas or PolyGraph replicas as their measured ability to accept work changes. Cheeger bounds constrain structural bottlenecks, while capacity preparation gives upcoming stages and node autoscalers notice of future demand. Application measurements and load tests establish useful targets for each boundary.

Measure the adaptation envelope

The same approach applies to a small producer-consumer pair and an application spread across clusters: respond close to the work, report what happened, and coordinate broader changes where dependencies are shared. Immediate admission and backpressure stay local; graph changes follow configured observation windows, cooldowns and resource limits. This gives a growing system ways to respond promptly without every reaction waiting for coordination across the whole application. Its adaptation envelope records which changes it can absorb, how quickly it recovers and which constraints it must preserve.

What Polyad abstracts

A graph groups related work and describes how its parts depend on each other. Its nodes can be tasks, services, resources or other graphs. A data pipeline might fetch records, process partitions in parallel, then publish the results; a service graph might keep consumers and their supporting resources running.[3]

Application composition

Polygraphs: graphs of graphs

Compose smaller workflows into an application with PolyGraph. Each child reports progress to its parent, giving the root a combined view of the work.[4]

In the diagrams below, green marks work and graph summaries, amber marks constraints or recurrence, and gray marks resources and containing boundaries.

Example: nested graphs reporting to an application root
---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart BT
    job["Workload"] --> batch["Graph · batch"]
    daemon["Daemon"] --> service["Graph · service"]
    spot["Graph · spot work"] --> group["PolyGraph · processing"]
    batch --> group
    group --> root["PolyGraph · application"]
    service --> root
    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    classDef constraint fill:#fff3d6,stroke:#926000,color:#513900
    class job,daemon,batch,service,group,root execution
    class spot constraint
Loading

Read about graphs of graphs.

Constrained compositions

Build workflows from reusable definitions and trace each instance to its Kubernetes resources. GraphPolicy lets engineers constrain what users can schedule by size, shape, nesting and mathematical properties.[5][6]

Example: reusable graph definitions with structural constraints
---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart LR
    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    classDef constraint fill:#fff3d6,stroke:#926000,color:#513900
    classDef resource fill:#eeeeee,stroke:#777777,color:#444444

    request["Composition request<br/>IDs and references"]:::resource
    rules["GraphPolicy<br/>size, shape, spectrum"]:::constraint
    root["PolyGraph root<br/>aggregate status"]:::execution
    request --> root
    rules -. "constrains each boundary" .-> root
    subgraph left["Graph instance: left"]
        a["Workload instance"]:::execution
    end
    subgraph right["Graph instance: right"]
        b["Workload instance"]:::execution
    end
    root --> a
    root --> b
    definition["Shared graph definition"]:::resource
    definition -. "instantiates" .-> a
    definition -. "instantiates" .-> b
Loading

Read about composition requests and GraphPolicy constraints.

Adaptive microservices

Writing Adaptive Microservices for execution in Polygraphs

Soul searching: adapt a service's workers

Application code can participate in adaptation, using available capacity while protecting work it has already accepted. The Soul study demonstrates this with six real Python service processes, a load generator and a monitoring parent. Each service uses the SDK's adaptation strategies to respond to changing demand and controlled disturbances.

The parent routes new jobs toward services with room to accept them. Services pause new assignments when observations become unavailable, peers become unhealthy or connection permission expires, while draining work already accepted. The study compares fixed and adaptive trials under the same offered load and resource ceilings, verifies every completed job, and records queue sizes, completion latency and process lifecycles.

Six services and their child workers before, during and after adapting to load and constraints

Build services that put spare capacity to work and release extra workers when demand falls. These recorded process graphs show services adding batch workers, switching to compact workers under modeled memory pressure and recovering their original footprint. Accepted jobs remain tracked through each transition. Click the figure to inspect the worker identities and connections.

Natural Selection: adapt service compositions

The Nature study adds composition decisions above those local adaptations. When the required output changes, its Natural Selection planner chooses which service implementations and connections can deliver it within the configured process and cost limits.

Service compositions and their child workers before, during and after the required output changes

Let services contribute in different ways as requirements change. A and F switch to implementations that perform both processing steps; B and E keep running and connect to new services that supply the second step. C and H retire after draining their accepted work. When the original requirement returns, the planner restores the original composition. Each selected service continues adapting its own workers. Click the figure to follow the process identities and routes.

Application responsibilities

For developers, the reusable pattern is to keep business processing separate from the policies that decide when to accept work, which worker profile to run and how to replace it safely. Extend AdaptiveService, choose strategies for your application's constraints, and measure whether those adaptations improve useful completion and recovery. The study's strategy modules and repeatable run instructions provide a working starting point with before, during and after measurements.

Workloads calling the operator

Running workloads can submit their next graph, read its status and subscribe to graph events through the operator's optional APIs. Services route requests to ready replicas; explicitly authorized callers can connect from other namespaces.[15]

A running service can also pulse downstream workloads or daemon replica groups, with explicit concurrency and frequency policies.[17]

The Python SDK supports filtered event hooks over SSE or WebSocket. With optional connection rebalancing, operators send paced reconnect instructions, called copulses, when membership changes or an administrator starts a roll. Clients retain their last completed checkpoint, refresh ready endpoints and reconnect through Service/Istio routing or direct client-side round robin. Graceful shutdown gives subscriptions a bounded window to reconnect. See the scale-out and scale-down sequence diagrams and typed Helm reference.

With a capacity policy, Polyad forecasts upcoming stages while earlier work runs, giving a compatible node autoscaler advance notice. Dependencies and gates still decide when the next stage starts.[16]

Example: API access, event streams and advance capacity requests
---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart TB
    subgraph local["Workloads · operator namespace"]
        caller["Managed workload"]
    end
    subgraph remote["Workloads · another namespace"]
        subscriber["Authorized workload"]
    end
    access["Explicit network access<br/>Optional Istio identity authorization"]
    composition["Composition Service · 8090<br/>Submit graphs and read status"]
    events["Event Service · 8091<br/>Subscribe to graph observations"]
    subgraph control["Node group · operators"]
        operator["Ready operator replicas<br/>APIs and graph scheduling"]
        cache[("Shared Dragonfly cache<br/>Queues and event history")]
    end
    demand["Kubernetes API<br/>ProvisioningRequest or placeholder Pods"]
    autoscaler["Node autoscaler"]
    machines["Target worker node group<br/>Capacity for upcoming stages"]
    caller -->|"Authenticated requests"| access
    subscriber -->|"Cross-namespace requests"| access
    access --> composition
    access -->|"GET /v1/events"| events
    composition --> operator
    events --> operator
    operator <-->|"Coordination and observations"| cache
    events -. "SSE or WebSocket observations" .-> local
    events -. "SSE or WebSocket observations" .-> remote
    operator -->|"Forecast before next stage"| demand
    demand -->|"Upcoming resource demand"| autoscaler
    autoscaler -->|"Provision nodes when supported"| machines
    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    classDef constraint fill:#ffe3a3,stroke:#926000,color:#513900
    classDef resource fill:#eeeeee,stroke:#777777,color:#444444
    class caller,subscriber execution
    class operator,access constraint
    class composition,events,cache,demand,autoscaler,machines resource
    style local fill:#ffffff,stroke:#667085,stroke-width:2px,color:#344054
    style remote fill:#e2e6ec,stroke:#667085,stroke-width:2px,color:#344054
    style control fill:#fff3d6,stroke:#926000,stroke-width:2px,color:#513900
    linkStyle default stroke:#475467,stroke-width:2px
Loading

Read about workload API access, event subscriptions, and advance capacity requests.

Scaling and capacity

Autoscaling the hierarchy

Choose the scaling target

KEDA can autoscale different levels of the hierarchy by targeting a ReplicaGroup's Kubernetes /scale subresource. The group's template determines what each additional replica creates:

  • Daemon replicas: another service instance, backed by its selected Deployment or StatefulSet. Set the Daemon's own replicas: 1 when each group copy should represent one desired Pod.
  • Graph replicas: another complete workflow or service graph, including its workloads, resources and internal connections.
  • PolyGraph or nested ReplicaGroup replicas: another composition of graphs or replica groups, allowing scaling at multiple levels of the same application.

For example, scale a worker pool inside a processing graph as its queue grows, and scale copies of the whole processing graph as demand for complete pipelines grows. Scale one group instance independently, or scale a shared definition to update every inheriting instance; see instance and shared scaling.

Check constraints before scale changes

Before creating or retiring copies, Polyad refreshes the owning graph family's topology and checks replica bounds and applicable GraphPolicies, including structural limits and Cheeger constraints. KEDA supplies the requested count; constraints can block its application. Target the ReplicaGroup to use these checks: directly autoscaling a generated Deployment or StatefulSet bypasses graph admission. See constraints before scaling.

Coordinate routing and remote replicas

When percentage routing is configured, a positive traffic assignment also blocks removing its destination. Drain its share to zero before scale-in; newly added copies need explicit routing assignments. See traffic balancing and scaling.

A ReplicaGroup of PolyGraphs can also scale a complete cross-cluster composition. Each destination can independently scale its own local groups. The multicluster scaling diagram shows where each cluster refreshes live values and enforces its own rules.

Demand-driven adaptation and preparation

Define the demand signal

Soul searching lets a Graph or PolyGraph respond to application demand within administrator-approved profiles. Demand defaults to offered work per second. Administrators can instead select an exact signal name and unit (such as queueDepth in jobs or activeSessions in sessions) and define the thresholds that select each profile. The complete resource example includes the Graph, its GraphPolicy and its Workload/Daemon definitions. An authorized application reporter supplies the measurements; configuring a signal does not automatically scrape it.

Select approved adaptation profiles

A profile combines a separate application Cheeger target with optional traffic percentages and capacity preparation settings. Observe reports recommendations; Adapt may apply approved changes. The default Shortfall trigger waits for completed throughput to fall behind. The optional Demand trigger allows preparation while throughput still keeps up, or while queued work awaits processing.

For example, sustained growth past an approved queue-depth threshold can select a denser connection layout, rebalance traffic and prepare three dependency stages ahead instead of one. Fresh samples, stabilization, cooldowns and change budgets govern those adjustments. Every change must satisfy live GraphPolicies and the fixed graph and operator capacity ceilings.

Separate control-loop responsibilities
Control Responsibility
Soul searching Select approved connection, traffic and preparation settings from demand
KEDA/HPA Request replica counts for the configured scaling target
Polyad capacity planner Prepare known upcoming execution nodes and coordinate workload admission
Kubernetes and the node autoscaler Schedule Pods and provision machines for scheduling demand

Soul searching works independently of KEDA and leaves replica counts to the existing scaling controller. Lookahead prepares upcoming work already described by the graph; it does not add spare application replicas to an entirely deployed service. Actual node provisioning still depends on the configured autoscaler.

See profile configuration and limits, the preparation sequence diagram, and the runnable example. For the distinction between hard structural bounds and application targets, see comparing Cheeger controls.

Connectivity and placement

Replica connections

Each ReplicaGroup can choose its own connection mode. Here, one subgraph connects whole graph replicas in a Ring; another combines daemon replicas in a Star with bidirectional connections and a FullMesh. Edges between enclosing graphs and groups are declared separately at their network boundaries.

Example: three replica layouts and connections across subgraphs
---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart TB
    subgraph application["PolyGraph · application"]
        direction TB
        subgraph processing["Graph · processing"]
            subgraph pipelines["pipelines · Ring"]
                direction LR
                p0["replica-0<br/>Graph instance"]
                p1["replica-1<br/>Graph instance"]
                p2["replica-2<br/>Graph instance"]
                p0 --> p1 --> p2 --> p0
            end
        end

        subgraph serving["Graph · serving"]
            direction TB
            subgraph routers["routers · Star"]
                direction LR
                s0["replica-0<br/>Daemon · hub"]
                s1["replica-1<br/>Daemon"]
                s2["replica-2<br/>Daemon"]
                s3["replica-3<br/>Daemon"]
                s0 <--> s1
                s0 <--> s2
                s0 <--> s3
            end

            subgraph caches["caches · FullMesh"]
                direction LR
                m0["replica-0<br/>Daemon"]
                m1["replica-1<br/>Daemon"]
                m2["replica-2<br/>Daemon"]
                m0 <--> m1
                m1 <--> m2
                m2 <--> m0
            end

            routers -->|"TCP 6379"| caches
        end

        processing -->|"TCP 9000"| serving
    end

    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    classDef boundary fill:#f2f4f7,stroke:#667085,color:#344054
    classDef replicas fill:#ffffff,stroke:#667085,stroke-width:2px,color:#344054
    class p0,p1,p2,s0,s1,s2,s3,m0,m1,m2 execution
    class processing,serving boundary
    class pipelines,routers,caches replicas
    style application fill:#ffffff,stroke:#667085,stroke-width:2px,color:#344054
    linkStyle default stroke:#475467,stroke-width:2px
Loading

The three inner boxes are ReplicaGroups. Their internal edges use these settings:

ReplicaGroup Copies Connection configuration Internal ports
pipelines Graph Ring TCP 8080
routers Daemon Star, bidirectional: true TCP 9000
caches Daemon FullMesh TCP 6379

Arrows show directed data-flow connections; double arrows declare both directions. The application boundary connects processing to serving on TCP 9000; the serving boundary connects routers to caches on TCP 6379. These connections become transport grants when graph networking is enabled, subject to inherited restrictions. Copies without a configured mode remain Independent, with no inter-copy edges. Scaling rebuilds the selected pattern and notifies workloads through topology events.

This example fits within one cluster. For remote Graph workloads, see the east-west gateway diagram and direct Pod routing diagram. Remote placement and data-flow edges need separately configured traffic policies.

Network boundaries

Group workloads into subgraphs with explicit network connections. Scoped rules control traffic across boundaries and namespaces; optional Istio integration adds HTTP and service-identity authorization.[7][8]

Optional percentage routing controls how incoming requests are divided among connected replicas, while network policies control which traffic is permitted. Polyad generates Istio VirtualServices and DestinationRules for local sidecar HTTP, HTTP/2 and gRPC Services. A Graph or PolyGraph copy receives its share through its entrypoint workloads. Try the traffic-balancing example, or compare fixed percentages with the Tiers and Headroom feedback modes.

Example: subgraph connections and cross-namespace authorization
---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart TB
    rules["GraphPolicy · subtree scope"] -. inherits .-> group
    subgraph group["PolyGraph · application"]
        direction TB
        subgraph producers["Graph · producers"]
            task["Workload"] --> sender["Daemon"]
        end
        subgraph consumers["Graph · consumers"]
            receiver["Daemon"] --> report["Workload"]
        end
        producers -->|"TCP 8080"| consumers
    end
    external["Service identity · another namespace"] -->|"GET /status"| consumers
    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    classDef constraint fill:#fff3d6,stroke:#926000,color:#513900
    classDef boundary fill:#f2f4f7,stroke:#667085,color:#344054
    class task,sender,receiver,report execution
    class rules,external constraint
    class group,producers,consumers boundary
Loading

Read about network scope and inheritance and cross-namespace authorization. This diagram shows one cluster; remote traffic rules are configured separately at each end of a cross-cluster connection.

Graphs across clusters

Graphs execute within one cluster. PolyGraphs can optionally place child Graphs and nested PolyGraphs in registered remote clusters, composing regions and higher levels. Optional shared observers expose read-only graph snapshots while each cluster's operator enforces its own rules and executes its workloads. See cross-cluster placement, Istio transport and observers.

A root operator control plane can manage this entire hierarchy from a separate management cluster. It installs remote execution replicas, collects their observations centrally and coordinates KEDA through root-local scale targets. See the deployment architecture and complete configuration example.

Example: nested PolyGraphs composing three clusters
---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart TB
    subgraph east["Cluster east"]
        root["PolyGraph<br/>Global application"]
        region["PolyGraph<br/>East region"]
        batch["Graph · batch<br/>Local Jobs"]
        root --> region --> batch
    end
    subgraph west["Cluster west"]
        group["PolyGraph<br/>Western regions"]
        service["Graph · service<br/>Local Deployments or StatefulSets"]
        group --> service
    end
    subgraph north["Cluster north"]
        analytics["Graph · analytics<br/>Local Jobs and resources"]
    end
    root -->|"cluster: west"| group
    group -->|"cluster: north"| analytics
    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    class root,region,batch,group,service,analytics execution
Loading

Arrows show declared parent-child ownership; child status rolls back up to the root. Each Graph's workloads stay inside its cluster. The east operator manages the western PolyGraph's intent; the west operator manages that PolyGraph's children, including the Graph in north. Each destination operator executes its local work. The same hierarchy can be entirely local by omitting cluster.

Read about graphs of graphs, remote placement and ownership, and the execution architecture. The two-cluster example demonstrates local nesting in east with a remote Graph in west; the diagram extends this pattern with another remote PolyGraph and cluster.

Graphs across node groups

Place whole graphs on groups of Kubernetes machines, such as general compute or accelerators. Here, three graphs share two worker groups while coordinated operator replicas and their shared Dragonfly cache run on a third.[9][10]

Example: three graphs across two worker groups
---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart TB
    subgraph control["Node group · operators"]
        operator["Polyad operator replicas"]
        cache[("Shared Redis / Dragonfly cache")]
    end
    api["Kubernetes API"]
    operator <-->|"queues and coordination"| cache
    operator -->|"ordered resource writes"| api
    subgraph groupA["Node group · general compute"]
        subgraph graphA["Graph · ingest"]
            fetch["Fetch"] --> normalize["Normalize"]
        end
        subgraph graphB["Graph · publish"]
            package["Package"] --> publish["Publish"]
        end
    end
    subgraph groupB["Node group · accelerated compute"]
        subgraph graphC["Graph · processing"]
            compute["Compute"] --> aggregate["Aggregate"]
        end
    end
    api -->|"graph placement"| graphA
    api -->|"graph placement"| graphB
    api -->|"graph placement"| graphC
    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    classDef constraint fill:#fff3d6,stroke:#926000,color:#513900
    classDef resource fill:#eeeeee,stroke:#777777,color:#444444
    class fetch,normalize,package,publish,compute,aggregate execution
    class operator constraint
    class api,cache resource
    style control fill:#fff3d6,stroke:#926000,color:#513900
    style groupA fill:#eeeeee,stroke:#777777,color:#444444
    style groupB fill:#eeeeee,stroke:#777777,color:#444444
Loading

Read about graph placement and operator replicas and shared queues.

Workload lifecycles

Finite pipelines

Express a workflow from preparation to publication, with parallel tasks and gates that wait for a condition or delay. Ordinary Graph placement can select spot capacity; applications choose how to handle interruption and storage.[11][12]

Example: parallel spot workloads behind an admission gate
---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart TB
    subgraph pipeline["Graph · finite pipeline"]
        direction TB
        storage[("Resource · shared storage")]
        prepare["Workload · prepare"]
        gate{"Gate · admission condition"}
        subgraph workers["Graph · spot placement"]
            direction TB
            left["Workload · partition A"]
            right["Workload · partition B"]
        end
        subgraph publish["Graph · publish results"]
            merge["Workload · merge"]
            report["Workload · report"]
            merge -->|completed| report
        end
        storage -->|ready| prepare
        prepare -->|completed| gate
        gate -->|allowed| left
        gate -->|allowed| right
        workers -->|completed| publish
    end
    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    classDef constraint fill:#ffe3a3,stroke:#926000,color:#513900
    classDef resource fill:#eeeeee,stroke:#777777,color:#444444
    class prepare,left,right,merge,report execution
    class gate constraint
    class storage resource
    style pipeline fill:#e2e6ec,stroke:#667085,stroke-width:2px,color:#344054
    style workers fill:#fff3d6,stroke:#926000,stroke-width:2px,color:#513900
    style publish fill:#ffffff,stroke:#667085,stroke-width:2px,color:#344054
    linkStyle default stroke:#475467,stroke-width:2px
Loading

Read about finite pipelines and admission and delay gates.

Persistent services and recurrence

Keep services running with Daemon, and repeat finite Graphs with activation requests. A producer or timer supplies each pulse; the application owns iteration limits, stop conditions and durable shared state.[13]

Example: persistent services with repeated graph activations

Solid arrows show startup or execution progression; dashed arrows show data flow or a new activation request.

---
config:
  theme: base
  htmlLabels: false
  themeVariables:
    primaryTextColor: "#163b29"
    secondaryTextColor: "#513900"
    tertiaryTextColor: "#344054"
    clusterBkg: "#f2f4f7"
    clusterBorder: "#667085"
    titleColor: "#344054"
    edgeLabelBackground: "#f2f4f7"
    lineColor: "#667085"
  flowchart:
    subGraphTitleMargin:
      top: 8
      bottom: 20
---
flowchart TB
    subgraph system["Graph · persistent service"]
        direction TB
        queue[("Resource · queue service")]
        subgraph service["Graph · persistent processing"]
            direction LR
            ingest["Daemon · ingest"]
            process["Daemon · process"]
            ingest -->|ready| process
            ingest <-.->|"events / acknowledgements"| process
        end
        subgraph activations["Graph · finite activation target"]
            direction LR
            sample["Workload · sample"]
            adjust["Workload · adjust"]
            sample -->|completed| adjust
        end
        next["Producer / timer pulse"]
        activations -->|"completion observed"| next
        next -.->|"activation request"| activations
        queue -->|ready| service
        service -->|ready| activations
    end
    classDef execution fill:#e3f3e8,stroke:#247047,color:#163b29
    classDef recurrence fill:#ffe3a3,stroke:#926000,color:#513900
    classDef resource fill:#eeeeee,stroke:#777777,color:#444444
    class ingest,process,sample,adjust execution
    class next recurrence
    class queue resource
    style system fill:#e2e6ec,stroke:#667085,stroke-width:2px,color:#344054
    style service fill:#ffffff,stroke:#667085,stroke-width:2px,color:#344054
    style activations fill:#fff3d6,stroke:#926000,stroke-width:2px,color:#513900
    linkStyle default stroke:#475467,stroke-width:2px
Loading

Read about repeated execution and activation pulses.

The operator can also request capacity ahead of upcoming stages, helping node autoscalers prepare machines while upstream work runs.

Control plane and operations

The operator as a Graph

Managed service components

Polyad can run as a compact HA Deployment or manage its own service components in a Graph. With architecture.mode: Distributed, gateway, executor and telemetry ReplicaGroups scale independently through KEDA and fresh GraphPolicy checks. A root bootstrap Deployment retains planning and recovery responsibility. With root mode enabled, the atlas, a reserved root PolyGraph, contains a Graph for each operator group. The root operator Graph contains the bootstrap, managed component pipeline, KEDA and all enabled local chart services; remote operator groups join as peers.

flowchart TB
    subgraph operators["Atlas · reserved root PolyGraph"]
        subgraph rootGroup["Graph · root operator group"]
            subgraph bootstrap["Graph · bootstrap observation"]
                root["Helm-owned root Deployment<br/>planning and recovery"]
            end
            subgraph self["Graph · local service components"]
                direction LR
                gateway["Gateway replicas<br/>APIs and event subscriptions"]
                executor["Executor replicas<br/>graph admission and workloads"]
                telemetry["Telemetry replicas<br/>observations and metrics"]
                gateway --> executor --> telemetry
            end
            bootstrap -->|"reconcile and restore components"| self
            self -->|"observations"| bootstrap
            keda["KEDA Graph<br/>Operator, metrics server and webhooks"]
            cache["Dragonfly Graph<br/>Controller, cache and Services"]
            pg["PostgreSQL Graph · optional<br/>State and authentication databases"]
            support["Other local service Graphs<br/>Endpoints, observer and Istio"]
            root <-->|"use and observe"| cache
            root <-->|"observe"| support
        end
        subgraph west["Graph · west workload cluster"]
            workers["DaemonSet · execution workers<br/>One Pod per eligible node"]
        end
        subgraph east["Graph · east workload cluster"]
            pool["Deployment · execution workers<br/>KEDA replica scaling"]
        end
        rootGroup -->|"root coordination"| west
        rootGroup -->|"root coordination"| east
        workers -->|"observations"| root
        pool -->|"observations"| root
    end
    rules["GraphPolicy<br/>Cheeger ≥ 1; recursive size bound"] -. constrains .-> self
    keda -->|"scrape demand"| telemetry
    keda -->|"request replica counts"| root
    root -. "optional state storage" .-> pg
    telemetry -. "persist graph state and parameters" .-> pg
Loading

The arrows inside the Graph describe logical stages; actual coordination uses Kubernetes and Dragonfly. Cheeger constrains topology, while queue backlog and HTTP demand drive capacity decisions. See the component and networking diagrams and deployable example.

Downstream operators and shared services

Adding an OperatorPool in a registered downstream cluster automatically links its Graph into the reserved root PolyGraph. Deployment pools support KEDA replica scaling; DaemonSet capacity follows node eligibility. The root Graph's bootstrap branch observes its existing Deployment, preserving Helm ownership and recovery. The nested component Graph retains its own Cheeger and replica-budget checks. Application event streams exclude this operator tree.

The local service inventory observes the chart's enabled infrastructure while Helm and upstream operators retain lifecycle ownership. Set keda.install: true to install the optional pinned KEDA dependency, or configure references to existing KEDA with the typed KEDA values.

Administrators can also install downstream workers with Helm and attach their existing Deployments. Helm retains installation and upgrades; each attachment explicitly chooses root/KEDA or downstream replica scaling.

Optional durable state

PostgreSQL is optional, disabled by default, and stores graph observations and tracked parameters when enabled. Its optional KEDA configuration scales CloudNativePG instances from operator connection counts, still scraped from the operator. Additional database instances provide standby/read capacity; writes continue through the primary. Encryption-at-rest settings select encrypted storage for managed state and authentication databases, using an administrator-provided StorageClass or GKE disks backed by a Cloud KMS key. Optional record encryption uses an administrator-provided public key to encrypt JSON payloads inside the operator before writing them to either managed or external PostgreSQL databases.

Metrics, traces and GitOps health

Queue pressure and graph hierarchies are available through the optional Prometheus and JSON metrics API.

Optional OpenTelemetry tracing exports API request, reconciliation and Kubernetes operation spans to an OTLP/HTTP collector, with configurable sampling and Secret-backed exporter credentials. The Helm chart can add Grafana Alloy or Prometheus Agent to collect component metrics, with Alloy also forwarding container logs and traces. Decision logs explain admissions, scaling, topology membership and conflicts, with trace correlation and independently enabled OTLP log export. KEDA can scale services or whole graph compositions through ReplicaGroup, using workload metrics served by the operator.

Argo CD and Flux health checks report graph and leaf health across nested applications.

What Polyad is not

Polyad coordinates application graphs alongside existing cluster components.

  • A general-purpose policy engine such as OPA. GraphPolicy constrains the graphs Polyad admits and the resources it compiles. It does not evaluate Rego, replace application authorization, or enforce policy on every Kubernetes API request. Cluster-wide admission policy remains a separate concern.[18][19]
  • A replacement for the Kubernetes scheduler or node autoscaler. Polyad controls when graph work is admitted and propagates placement constraints. Kubernetes places Pods; the configured autoscaler provisions machines. Grouping work does not guarantee that every Pod starts together.[9][20]
  • A service mesh or network transport. Polyad generates network and Istio authorization and routing resources, including optional percentage splits. The cluster's networking implementation and mesh enforce them; drawing a graph connection does not transport application data.[21]
  • Automatic process checkpointing or exactly-once execution. Restarting containers with persistent storage requires application recovery logic. Workloads must handle retries and duplicate effects; graph ownership and ordered API writes do not make application operations transactional.[22][10]

License

GNU General Public License v3.0 only.

References

Selected background reading for Polyad's architecture and less common design choices.

About

Kubernetes operator and Python SDK for adaptive microservices. Compose graphs across clusters, enforce Cheeger bounds, and adapt topology, traffic and capacity to application demand.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages