Skip to main content

GraphQL API

API Reference

GraphQL API

EnerOS provides a GraphQL interface via the eneros-graphql crate, with the endpoint POST /graphql. GraphQL is suitable for scenarios that require flexible field selection and multi-type aggregate queries, avoiding the over-fetching / under-fetching problems of REST. EnerOS’s GraphQL implementation is based on async-graphql, fully asynchronous and zero-copy, sharing the same set of system calls with the kernel.

Endpoint Information

ItemValue
URLhttp://localhost:8080/graphql
MethodPOST
Content-Typeapplication/json
AuthenticationBearer Token (Header) or mTLS
Subscription Endpointws://localhost:8080/graphql (WebSocket Subscriptions protocol)
IntrospectionEnabled in development, disabled by default in production
Query Depth Limit7 levels
Complexity Limit1000
Query Timeout30 seconds
Alias Limit Per Request20

Request Format

The GraphQL request body is uniformly JSON, containing the following fields:

FieldTypeRequiredDefaultDescription
querystringYesGraphQL query string
variablesobjectNonullVariable dictionary
operationNamestringNoSpecified when query contains multiple operations
extensionsobjectNoExtension fields (e.g., persisted queries)

Complete Schema

Scalar Types

In addition to standard scalars (String/Int/Float/Boolean/ID), EnerOS defines the following custom scalars:

scalar DateTime    # RFC 3339 time string
scalar JSON        # Any JSON value
scalar Float64     # IEEE 754 double
scalar UInt64      # 64-bit unsigned integer
scalar Decimal     # High-precision decimal (for electrical quantity calculations)

Enum Types

enum NetworkStatus {
  DRAFT
  READY
  LOCKED
  ARCHIVED
}

enum AgentStatus {
  IDLE
  RUNNING
  PAUSED
  ERROR
  STOPPED
}

enum AgentType {
  DISPATCH
  SELF_HEALING
  TRADING
  MAINTENANCE
  PLANNING
}

enum BusType {
  SLACK
  PV
  PQ
}

enum DeviceType {
  PV
  WIND
  BESS
  EV
  LOAD
  TRANSFORMER
  BREAKER
}

enum PowerFlowMethod {
  NEWTON_RAPHSON
  FAST_DECOUPLED
  DC
}

enum PowerFlowStatus {
  QUEUED
  RUNNING
  CONVERGED
  DIVERGED
  TIMEOUT
  CANCELLED
}

enum Aggregation {
  NONE
  AVG
  SUM
  MAX
  MIN
  COUNT
  PERCENTILE_50
  PERCENTILE_95
  PERCENTILE_99
}

enum Severity {
  INFO
  WARN
  ERROR
  CRITICAL
}

enum AlarmStatus {
  ACTIVE
  ACKNOWLEDGED
  CLEARED
}

enum CommandPriority {
  LOW
  NORMAL
  HIGH
  EMERGENCY
}

enum BranchStatus {
  CLOSED
  OPEN
  MAINTENANCE
}

Input Types

input NetworkFilter {
  status: NetworkStatus
  nameContains: String
  tags: [TagFilterInput!]
  createdAfter: DateTime
  createdBefore: DateTime
}

input TagFilterInput {
  key: String!
  value: String
}

input NetworkInput {
  name: String!
  description: String
  baseKv: Float
  baseMva: Float
  buses: [BusInput!]!
  branches: [BranchInput!]
  generators: [GeneratorInput!]
  loads: [LoadInput!]
  tags: JSON
}

input BusInput {
  id: Int!
  type: BusType!
  v: Float
  theta: Float
  area: Int
}

input BranchInput {
  from: Int!
  to: Int!
  r: Float!
  x: Float!
  b: Float
  status: BranchStatus
}

input GeneratorInput {
  bus: Int!
  mw: Float!
  mvar: Float
  pmin: Float
  pmax: Float
  qmin: Float
  qmax: Float
}

input LoadInput {
  bus: Int!
  mw: Float!
  mvar: Float
}

input AgentInput {
  name: String!
  type: AgentType!
  networkId: ID
  config: JSON
  tools: [String!]
  reasoningModel: String
  maxActionsPerMinute: Int
  autoStart: Boolean
}

input CommandInput {
  command: String!
  params: JSON
  priority: CommandPriority
  timeoutMs: Int
  awaitResult: Boolean
}

input TimeSeriesQuery {
  metric: String!
  tags: [TagFilterInput!]
  from: DateTime!
  to: DateTime!
  aggregation: Aggregation
  interval: String
  fill: String
  limit: Int
}

Object Types

type Network {
  id: ID!
  name: String!
  description: String
  baseKv: Float!
  baseMva: Float!
  busCount: Int!
  branchCount: Int!
  generatorCount: Int!
  loadCount: Int!
  status: NetworkStatus!
  tags: JSON
  buses: [Bus!]!
  branches: [Branch!]!
  generators: [Generator!]!
  loads: [Load!]!
  createdAt: DateTime!
  updatedAt: DateTime!
  powerFlow(method: PowerFlowMethod): PowerFlowResult
  agents: [Agent!]!
  alarms(severity: Severity): [Alarm!]!
}

type Bus {
  id: Int!
  type: BusType!
  voltage: Voltage!
  area: Int
  generators: [Generator!]!
  loads: [Load!]!
}

type Voltage {
  magnitude: Float!
  angle: Float!
}

type Branch {
  from: Int!
  to: Int!
  r: Float!
  x: Float!
  b: Float!
  status: BranchStatus!
  flow: BranchFlow
}

type BranchFlow {
  pFrom: Float!
  qFrom: Float!
  pTo: Float!
  qTo: Float!
  loading: Float!
}

type Generator {
  bus: Int!
  mw: Float!
  mvar: Float!
  pmin: Float
  pmax: Float
  qmin: Float
  qmax: Float
}

type Load {
  bus: Int!
  mw: Float!
  mvar: Float!
}

type Agent {
  id: ID!
  name: String!
  type: AgentType!
  networkId: ID
  network: Network
  status: AgentStatus!
  config: JSON
  tools: [String!]!
  reasoningModel: String!
  maxActionsPerMinute: Int!
  createdAt: DateTime!
  startedAt: DateTime
  lastActiveAt: DateTime
  actions: [AgentAction!]!
}

type AgentAction {
  id: ID!
  agentId: ID!
  command: String!
  params: JSON
  status: String!
  result: JSON
  executedAt: DateTime!
  durationMs: Int!
}

type PowerFlowResult {
  taskId: ID!
  networkId: ID!
  status: PowerFlowStatus!
  method: PowerFlowMethod!
  iterations: Int
  durationMs: Int!
  tolerance: Float!
  buses: [BusResult!]!
  branches: [BranchResult!]!
  losses: Losses
  startedAt: DateTime!
  completedAt: DateTime
}

type BusResult {
  id: Int!
  v: Float!
  theta: Float!
  pGen: Float
  qGen: Float
  pLoad: Float
  qLoad: Float
}

type BranchResult {
  from: Int!
  to: Int!
  pFrom: Float!
  qFrom: Float!
  pTo: Float!
  qTo: Float!
  loading: Float!
}

type Losses {
  pMw: Float!
  qMvar: Float!
}

type Device {
  id: ID!
  name: String!
  type: DeviceType!
  status: String!
  networkId: ID
  bus: Int
  capacityMw: Float
  online: Boolean!
  metadata: JSON
  lastSeenAt: DateTime
}

type TimeSeriesPoint {
  timestamp: DateTime!
  value: Float!
  tags: JSON
}

type Alarm {
  id: ID!
  severity: Severity!
  status: AlarmStatus!
  type: String!
  message: String!
  networkId: ID
  source: AlarmSource!
  details: JSON
  raisedAt: DateTime!
  acknowledgedAt: DateTime
  acknowledgedBy: String
  clearedAt: DateTime
}

type AlarmSource {
  type: String!
  id: ID!
}

type CommandResult {
  commandId: ID!
  agentId: ID!
  command: String!
  status: String!
  result: JSON
  durationMs: Int!
  executedAt: DateTime!
}

Query Type

type Query {
  # Network
  networks(filter: NetworkFilter, page: Int, pageSize: Int): NetworkConnection!
  network(id: ID!): Network

  # Agent
  agents(status: AgentStatus, type: AgentType, page: Int, pageSize: Int): AgentConnection!
  agent(id: ID!): Agent

  # Device
  devices(type: DeviceType, online: Boolean, page: Int, pageSize: Int): DeviceConnection!
  device(id: ID!): Device

  # Time Series
  timeseries(query: TimeSeriesQuery!): [TimeSeriesPoint!]!
  timeseriesMulti(queries: [TimeSeriesQuery!]!): [[TimeSeriesPoint!]!]!

  # Events and Alarms
  events(topic: String, from: DateTime, to: DateTime, page: Int, pageSize: Int): EventConnection!
  alarms(severity: Severity, status: AlarmStatus, from: DateTime, to: DateTime): [Alarm!]!
  alarm(id: ID!): Alarm

  # Audit
  audit(actor: String, action: String, from: DateTime, to: DateTime, page: Int, pageSize: Int): AuditConnection!

  # Health
  health: HealthStatus!
}

type NetworkConnection {
  items: [Network!]!
  total: Int!
  page: Int!
  pageSize: Int!
}

type AgentConnection {
  items: [Agent!]!
  total: Int!
  page: Int!
  pageSize: Int!
}

type DeviceConnection {
  items: [Device!]!
  total: Int!
  page: Int!
  pageSize: Int!
}

type EventConnection {
  items: [Event!]!
  total: Int!
  page: Int!
  pageSize: Int!
}

type AuditConnection {
  items: [AuditEntry!]!
  total: Int!
  page: Int!
  pageSize: Int!
}

type Event {
  id: ID!
  topic: String!
  kind: String!
  payload: JSON!
  timestamp: DateTime!
}

type AuditEntry {
  id: ID!
  timestamp: DateTime!
  actorType: String!
  actorId: String!
  tenant: String!
  action: String!
  resourceType: String!
  resourceId: ID!
  requestId: ID!
  ip: String
  userAgent: String
  result: String!
  details: JSON
}

type HealthStatus {
  status: String!
  version: String!
  uptimeSeconds: Int!
  checks: JSON!
}

Mutation Type

type Mutation {
  # Network
  createNetwork(input: NetworkInput!): Network!
  updateNetwork(id: ID!, input: NetworkInput!): Network!
  deleteNetwork(id: ID!): Boolean!
  validateNetwork(id: ID!): ValidationResult!

  # Power Flow
  runPowerFlow(id: ID!, method: PowerFlowMethod, async: Boolean): PowerFlowResult!
  cancelTask(taskId: ID!): Boolean!

  # Agent
  createAgent(input: AgentInput!): Agent!
  updateAgent(id: ID!, input: AgentInput!): Agent!
  deleteAgent(id: ID!): Boolean!
  pauseAgent(id: ID!): Agent!
  resumeAgent(id: ID!): Agent!
  sendCommand(agentId: ID!, command: CommandInput!): CommandResult!

  # Alarm
  acknowledgeAlarm(id: ID!, note: String): Alarm!

  # Time Series Write
  writeTimeseries(points: [TimeSeriesPointInput!]!): WriteResult!
}

type ValidationResult {
  valid: Boolean!
  errors: [ValidationError!]
}

type ValidationError {
  field: String!
  message: String!
  code: String!
}

input TimeSeriesPointInput {
  metric: String!
  timestamp: DateTime
  value: Float!
  tags: JSON
}

type WriteResult {
  accepted: Int!
  rejected: Int!
  errors: [WriteError!]
}

type WriteError {
  index: Int!
  message: String!
}

Subscription Type

type Subscription {
  networkChanged(id: ID): NetworkEvent!
  agentStatusChanged(id: ID): AgentStatusEvent!
  alarmStream(severity: Severity): Alarm!
  eventStream(topic: String): Event!
  powerFlowProgress(taskId: ID!): PowerFlowProgress!
}

type NetworkEvent {
  networkId: ID!
  kind: String!
  changes: JSON!
  timestamp: DateTime!
}

type AgentStatusEvent {
  agentId: ID!
  oldStatus: AgentStatus!
  newStatus: AgentStatus!
  reason: String
  timestamp: DateTime!
}

type PowerFlowProgress {
  taskId: ID!
  iteration: Int!
  maxIterations: Int!
  mismatch: Float!
  timestamp: DateTime!
}

Query Examples

Query Network with Power Flow Results

query GetNetworkWithPowerFlow($id: ID!) {
  network(id: $id) {
    id
    name
    baseKv
    status
    buses {
      id
      type
      voltage { magnitude angle }
    }
    branches { from to status }
    powerFlow(method: NEWTON_RAPHSON) {
      status
      iterations
      durationMs
      buses { id v theta }
      branches { from to loading }
      losses { pMw qMvar }
    }
  }
}

Variables:

{
  "id": "net_8f3a2b"
}

Response:

{
  "data": {
    "network": {
      "id": "net_8f3a2b",
      "name": "IEEE-14",
      "baseKv": 110.0,
      "status": "READY",
      "buses": [
        {"id": 1, "type": "SLACK", "voltage": {"magnitude": 1.05, "angle": 0.0}},
        {"id": 2, "type": "PV", "voltage": {"magnitude": 1.04, "angle": -0.039}}
      ],
      "branches": [{"from": 1, "to": 2, "status": "CLOSED"}],
      "powerFlow": {
        "status": "CONVERGED",
        "iterations": 4,
        "durationMs": 12,
        "buses": [
          {"id": 1, "v": 1.05, "theta": 0.0},
          {"id": 2, "v": 1.04, "theta": -0.039}
        ],
        "branches": [{"from": 1, "to": 2, "loading": 0.45}],
        "losses": {"pMw": 1.23, "qMvar": 5.67}
      }
    }
  }
}

Time Series Aggregation Query

query GetTimeSeries($query: TimeSeriesQuery!) {
  timeseries(query: $query) {
    timestamp
    value
    tags
  }
}

Variables:

{
  "query": {
    "metric": "branch_loading",
    "tags": [{"key": "network", "value": "net_8f3a2b"}, {"key": "branch", "value": "1-2"}],
    "from": "2026-07-06T00:00:00Z",
    "to": "2026-07-06T08:00:00Z",
    "aggregation": "AVG",
    "interval": "5m"
  }
}

Multi Time Series Parallel Query

query GetMultiTimeSeries($queries: [TimeSeriesQuery!]!) {
  timeseriesMulti(queries: $queries) {
    timestamp
    value
  }
}

Variables:

{
  "queries": [
    {"metric": "branch_loading", "from": "2026-07-06T00:00:00Z", "to": "2026-07-06T08:00:00Z", "aggregation": "AVG", "interval": "5m"},
    {"metric": "bus_voltage", "from": "2026-07-06T00:00:00Z", "to": "2026-07-06T08:00:00Z", "aggregation": "AVG", "interval": "5m"}
  ]
}

Using Fragments

fragment NetworkBasic on Network {
  id
  name
  status
  busCount
  createdAt
}

query GetNetworks {
  networks(filter: {status: READY}) {
    items {
      ...NetworkBasic
    }
    total
  }
}

Using Aliases

query GetMultipleNetworks {
  net1: network(id: "net_8f3a2b") {
    id
    name
    status
  }
  net2: network(id: "net_9c4d1e") {
    id
    name
    status
  }
}

Mutation Examples

Create Network

mutation CreateNetwork($input: NetworkInput!) {
  createNetwork(input: $input) {
    id
    name
    status
    busCount
    branchCount
    createdAt
  }
}

Variables:

{
  "input": {
    "name": "IEEE-14",
    "baseKv": 110.0,
    "baseMva": 100.0,
    "buses": [
      {"id": 1, "type": "SLACK", "v": 1.05, "theta": 0.0},
      {"id": 2, "type": "PV", "v": 1.04},
      {"id": 3, "type": "PQ"}
    ],
    "branches": [
      {"from": 1, "to": 2, "r": 0.01938, "x": 0.05917, "b": 0.0528}
    ],
    "tags": {"owner": "ieee"}
  }
}

Trigger Power Flow Calculation

mutation RunPowerFlow($id: ID!, $method: PowerFlowMethod) {
  runPowerFlow(id: $id, method: $method, async: false) {
    taskId
    status
    iterations
    durationMs
    buses { id v theta }
    branches { from to loading }
    losses { pMw qMvar }
  }
}

Create Agent and Send Command

mutation CreateAgentAndCommand($agentInput: AgentInput!, $commandInput: CommandInput!) {
  createAgent(input: $agentInput) {
    id
    name
    status
  }
}

mutation SendCommand($agentId: ID!, $command: CommandInput!) {
  sendCommand(agentId: $agentId, command: $command) {
    commandId
    status
    result
    durationMs
  }
}

Write Time Series Data

mutation WriteTimeseries($points: [TimeSeriesPointInput!]!) {
  writeTimeseries(points: $points) {
    accepted
    rejected
    errors { index message }
  }
}

Variables:

{
  "points": [
    {"metric": "branch_loading", "value": 0.42, "tags": {"network": "net_8f3a2b", "branch": "1-2"}},
    {"metric": "bus_voltage", "value": 1.05, "tags": {"network": "net_8f3a2b", "bus": "1"}}
  ]
}

Subscription Examples

Subscribe to Alarm Stream

subscription AlarmStream($severity: Severity) {
  alarmStream(severity: $severity) {
    id
    severity
    type
    message
    networkId
    raisedAt
    source { type id }
  }
}

Variables:

{
  "severity": "CRITICAL"
}

Subscribe to Agent Status Changes

subscription AgentStatus($agentId: ID) {
  agentStatusChanged(id: $agentId) {
    agentId
    oldStatus
    newStatus
    reason
    timestamp
  }
}

Subscribe to Power Flow Progress

subscription PowerFlowProgress($taskId: ID!) {
  powerFlowProgress(taskId: $taskId) {
    taskId
    iteration
    maxIterations
    mismatch
    timestamp
  }
}

Multi-Language Code Examples

cURL

curl -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "query": "query GetNetworks { networks { items { id name status } total } }"
  }'

Rust

use graphql_client::{GraphQLQuery, Response};
use serde::Serialize;

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "schema.json",
    query_path = "queries/get_networks.graphql",
    response_derives = "Debug"
)]
pub struct GetNetworks;

#[derive(Serialize)]
pub struct Vars;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = reqwest::Client::new();
    let req_body = GetNetworks::build_query(Vars {});

    let resp = client
        .post("http://localhost:8080/graphql")
        .bearer_auth("<token>")
        .json(&req_body)
        .send()
        .await?;

    let response_body: Response<get_networks::ResponseData> = resp.json().await?;
    if let Some(data) = response_body.data {
        for net in data.networks.items {
            println!("{}: {} ({:?})", net.id, net.name, net.status);
        }
    }
    Ok(())
}

Python

import requests

url = "http://localhost:8080/graphql"
headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer <token>",
}

query = """
query GetNetworks {
  networks {
    items { id name status }
    total
  }
}
"""

response = requests.post(url, json={"query": query}, headers=headers)
data = response.json()
for net in data["data"]["networks"]["items"]:
    print(f"{net['id']}: {net['name']} ({net['status']})")

JavaScript

import { graphqlRequest } from '@eneros/sdk';

const query = `
  query GetNetworks {
    networks {
      items { id name status }
      total
    }
  }
`;

const data = await graphqlRequest({
  endpoint: 'http://localhost:8080/graphql',
  query,
  token: '<bearer-token>',
});
console.log(data.networks.items);

Subscription (JavaScript)

import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'ws://localhost:8080/graphql',
  connectionParams: {
    token: '<bearer-token>',
  },
});

const unsubscribe = client.subscribe(
  {
    query: `
      subscription OnAlarm($severity: Severity) {
        alarmStream(severity: $severity) {
          id
          severity
          message
          raisedAt
        }
      }
    `,
    variables: { severity: 'CRITICAL' },
  },
  {
    next: (data) => console.log('Alarm:', data),
    error: (err) => console.error('Subscription error:', err),
    complete: () => console.log('Subscription ended'),
  }
);

Error Handling

GraphQL errors follow the specification, returning HTTP 200 + errors field:

{
  "errors": [
    {
      "message": "Network not found: net_unknown",
      "locations": [{"line": 2, "column": 3}],
      "path": ["network"],
      "extensions": {
        "code": "NOT_FOUND",
        "trace_id": "trace_abc123",
        "details": {"id": "net_unknown"}
      }
    }
  ],
  "data": null
}

Error Code Table

Error CodeMeaningSuggested Handling
UNAUTHENTICATEDNot authenticatedAdd Authorization Header
FORBIDDENInsufficient permissionsAdjust role permissions
NOT_FOUNDResource does not existVerify ID
VALIDATION_FAILEDField validation failedCheck error details
CONSTRAINT_VIOLATIONSafety constraint violatedCorrect electrical parameters
RATE_LIMITEDRate limit triggeredBackoff and retry
QUERY_TOO_COMPLEXQuery complexity exceededSplit the query
QUERY_TOO_DEEPQuery depth exceededReduce nesting
INTERNAL_ERRORInternal errorContact operations
POWERFLOW_DIVERGEDPower flow did not convergeAdjust initial values

Partial Success Response

When querying multiple fields, some may succeed while others fail. Failed fields in data will be null:

{
  "data": {
    "network": null,
    "agents": {"items": [], "total": 0}
  },
  "errors": [
    {
      "message": "Network not found",
      "path": ["network"],
      "extensions": {"code": "NOT_FOUND"}
    }
  ]
}

Introspection

Development environments support Introspection, allowing Schema metadata queries:

query IntrospectSchema {
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      name
      kind
      fields {
        name
        type { name kind }
      }
    }
  }
}

query IntrospectType($name: String!) {
  __type(name: $name) {
    name
    kind
    fields {
      name
      type { name kind ofType { name kind } }
      args { name type { name kind } }
    }
    enumValues { name }
  }
}

Can be enabled in production via configuration:

[graphql]
introspection = true

Performance Recommendations

  • Query depth limited to 7 levels, complexity limit 1000, to prevent malicious nesting
  • Time series queries should specify aggregation to avoid returning raw high-frequency samples
  • Subscription auto-reconnects on disconnection, replaying the latest 100 events after reconnection
  • Use persisted queries: High-frequency queries can be pre-registered to reduce network transmission
# Persisted query request
{
  "extensions": {
    "persistedQuery": {
      "sha256Hash": "abc123...",
      "version": 1
    }
  }
}