Skip to main content

19 posts tagged with "provider"

View All Tags

New Railway Provider Available

· 8 min read
Technologist and Cloud Consultant

We've released a new StackQL provider for Railway:

  • railway - the Railway public API: projects, environments, services, deployments, variables, networking, storage, billing, observability, workspaces, account, templates, integrations, platform and agents (15 services, 151 resources, 396 operations)

The provider reads and writes. Projects, environments, services, variables, domains and volumes can be queried, created, changed and removed, and deployment operations such as redeploy, restart and rollback are available on the resources they act on. The rest of this post is the questions it answers and the tasks it handles.

Connect​

Authentication is a Railway account token or workspace token, read from RAILWAY_TOKEN - the same variable the Terraform Railway provider uses:

export RAILWAY_TOKEN=...
stackql shell

Tokens are created under Account settings -> Tokens. A project token does not work with the provider. Railway limits API requests per hour by plan (100 on Free, 1000 on Hobby, 10000 on Pro), which is worth keeping in mind for queries that cover many projects or environments.

What is deployed where​

The workspaces a token can reach, the projects in one, and the environments of a project:

SELECT id, name, plan
FROM railway.workspaces.workspaces;

SELECT id, name, description, created_at
FROM railway.projects.projects
WHERE workspace_id = '<workspace-id>'
ORDER BY created_at DESC;

SELECT id, name, is_ephemeral
FROM railway.environments.environments
WHERE project_id = '<project-id>';

A service instance is a service as it is configured in one environment. Listing the instances of an environment shows what runs there, from which image or repository, and how the last deployment went:

SELECT service_name, region, num_replicas,
json_extract(source, '$.image') AS image,
json_extract(source, '$.repo') AS repo,
json_extract(latest_deployment, '$.status') AS last_deployment,
json_extract(latest_deployment, '$.created_at') AS deployed_at
FROM railway.services.service_instances
WHERE environment_id = '<environment-id>';

An IN list covers several environments in one query, with one request made per environment. Joining to environments puts names on the rows:

SELECT e.name AS environment, s.service_name, s.region, s.num_replicas,
json_extract(s.latest_deployment, '$.status') AS last_deployment
FROM railway.services.service_instances s
JOIN railway.environments.environments e ON e.id = s.environment_id
WHERE e.project_id = '<project-id>'
AND s.environment_id IN ('<production-environment-id>', '<staging-environment-id>')
ORDER BY environment, service_name;

The same works across projects, here for the services of two projects in a workspace:

SELECT p.name AS project, s.name AS service, s.created_at
FROM railway.services.services s
JOIN railway.projects.projects p ON p.id = s.project_id
WHERE p.workspace_id = '<workspace-id>'
AND s.project_id IN ('<project-id>', '<other-project-id>')
ORDER BY project, service;

Deployment health​

Deployment outcomes for a project, then the ones that failed or crashed, with who triggered them:

SELECT status, count(*) AS deployments
FROM railway.deployments.deployments
WHERE project_id = '<project-id>'
GROUP BY status;

SELECT id, service_id, status, created_at,
json_extract(creator, '$.name') AS deployed_by
FROM railway.deployments.deployments
WHERE project_id = '<project-id>'
AND status = '{in: [FAILED, CRASHED]}'
ORDER BY created_at DESC;

Services in an environment whose latest deployment is not healthy:

SELECT service_name,
json_extract(latest_deployment, '$.id') AS deployment_id,
json_extract(latest_deployment, '$.status') AS status
FROM railway.services.service_instances
WHERE environment_id = '<environment-id>'
AND json_extract(latest_deployment, '$.status') NOT IN ('SUCCESS', 'SLEEPING');

From there, the build and runtime logs of a deployment are tables. LIMIT sets how many lines are fetched:

SELECT timestamp, severity, message
FROM railway.observability.build_logs
WHERE deployment_id = '<deployment-id>'
LIMIT 100;

SELECT timestamp, severity, message
FROM railway.observability.deployment_logs
WHERE deployment_id = '<deployment-id>'
AND filter = 'error'
LIMIT 100;

Drift between environments​

Variables are one row per variable, so comparing two environments is a join. Variables a service has in production that are missing in staging:

SELECT prod.name
FROM railway.variables.variables prod
LEFT JOIN railway.variables.variables stg
ON stg.name = prod.name
WHERE prod.project_id = '<project-id>'
AND prod.environment_id = '<production-environment-id>'
AND prod.service_id = '<service-id>'
AND stg.project_id = '<project-id>'
AND stg.environment_id = '<staging-environment-id>'
AND stg.service_id = '<service-id>'
AND stg.name IS NULL
AND prod.name NOT LIKE 'RAILWAY_%';

Variables set in both environments with different values:

SELECT prod.name
FROM railway.variables.variables prod
JOIN railway.variables.variables stg
ON stg.name = prod.name
WHERE prod.project_id = '<project-id>'
AND prod.environment_id = '<production-environment-id>'
AND prod.service_id = '<service-id>'
AND stg.project_id = '<project-id>'
AND stg.environment_id = '<staging-environment-id>'
AND stg.service_id = '<service-id>'
AND prod.value <> stg.value
AND prod.name NOT LIKE 'RAILWAY_%';

The variables Railway provides itself are prefixed RAILWAY_ and differ between environments by design, so they are left out. Both queries return names only. Selecting value returns the values in clear, so treat that output as a secret.

The same approach compares how a service is configured in each environment:

SELECT prod.service_name,
prod.num_replicas AS prod_replicas, stg.num_replicas AS staging_replicas,
prod.region AS prod_region, stg.region AS staging_region,
prod.start_command AS prod_start_command, stg.start_command AS staging_start_command
FROM railway.services.service_instances prod
JOIN railway.services.service_instances stg
ON stg.service_id = prod.service_id
WHERE prod.environment_id = '<production-environment-id>'
AND stg.environment_id = '<staging-environment-id>';

Domains and certificates​

The generated domains of every service in an environment, without going service by service. The custom_domains key of the same column holds the custom ones:

SELECT s.service_name,
json_extract(d.value, '$.domain') AS domain,
json_extract(d.value, '$.target_port') AS target_port
FROM railway.services.service_instances s,
json_each(json_extract(s.domains, '$.service_domains')) d
WHERE s.environment_id = '<environment-id>';

Custom domains carry their verification and certificate state, and the DNS records Railway expects to find:

SELECT domain,
json_extract(status, '$.verified') AS verified,
json_extract(status, '$.certificate_status') AS certificate_status,
json_extract(status, '$.dns_records') AS dns_records
FROM railway.networking.custom_domains
WHERE project_id = '<project-id>'
AND environment_id = '<environment-id>'
AND service_id = '<service-id>';

Usage and cost​

Usage for a workspace by measurement, grouped by project, with project names joined in:

SELECT p.name AS project, u.measurement, u.value
FROM railway.billing.usage u
JOIN railway.projects.projects p
ON p.id = json_extract(u.tags, '$.project_id')
WHERE u.workspace_id = '<workspace-id>'
AND u.measurements = '[CPU_USAGE, MEMORY_USAGE_GB, NETWORK_TX_GB, DISK_USAGE_GB]'
AND u.group_by = '[PROJECT_ID]'
AND p.workspace_id = '<workspace-id>'
ORDER BY project, measurement;

start_date and end_date narrow the window. The projected usage for the current billing period, and the billing position of the workspace:

SELECT project_id, measurement, estimated_value
FROM railway.billing.estimated_usage
WHERE workspace_id = '<workspace-id>'
AND measurements = '[CPU_USAGE, MEMORY_USAGE_GB]';

SELECT state, current_usage, credit_balance,
json_extract(billing_period, '$.end') AS period_ends,
json_extract(usage_limit, '$.hard_limit') AS hard_limit
FROM railway.billing.customers
WHERE workspace_id = '<workspace-id>';

Volumes report their allocated and used size per environment:

SELECT json_extract(volume, '$.name') AS volume, mount_path, region, state,
size_mb, current_size_mb,
ROUND(100.0 * current_size_mb / size_mb, 1) AS used_pct
FROM railway.storage.volume_instances
WHERE environment_id = '<environment-id>';

Provisioning​

A project is created with one environment. RETURNING gives back the identifiers the next statements need:

INSERT INTO railway.projects.projects (name, description, workspace_id)
SELECT 'orders', 'order processing', '<workspace-id>'
RETURNING id, name, primary_environment_id;

INSERT INTO railway.environments.environments (project_id, name)
SELECT '<project-id>', 'staging'
RETURNING id, name;

A service from a container image or a repository. A service with a source is deployed when it is created, and a running deployment is billed:

INSERT INTO railway.services.services (project_id, name, source)
SELECT '<project-id>', 'cache', '{"image": "redis:7-alpine"}'
RETURNING id, name;

INSERT INTO railway.services.services (project_id, name, source, branch)
SELECT '<project-id>', 'api', '{"repo": "my-org/orders-api"}', 'main'
RETURNING id, name;

Variables are written one at a time or as a set. Writing a variable that exists replaces its value, and skip_deploys keeps the change from triggering a deployment:

INSERT INTO railway.variables.variables
(project_id, environment_id, service_id, name, value, skip_deploys)
SELECT '<project-id>', '<environment-id>', '<service-id>', 'LOG_LEVEL', 'info', true;

INSERT INTO railway.variables.variables
(project_id, environment_id, service_id, variables, skip_deploys)
SELECT '<project-id>', '<environment-id>', '<service-id>',
'{"SENTRY_DSN": "https://example.ingest.sentry.io/1", "FEATURE_FLAGS": "checkout-v2"}', true;

A generated domain and a volume for the service:

INSERT INTO railway.networking.service_domains (environment_id, service_id)
SELECT '<environment-id>', '<service-id>'
RETURNING id, domain;

INSERT INTO railway.storage.volumes (project_id, environment_id, service_id, mount_path)
SELECT '<project-id>', '<environment-id>', '<service-id>', '/data'
RETURNING id, name;

Day-two operations​

Changing how a service runs in an environment is an UPDATE. Values in SET are written as quoted strings:

UPDATE railway.services.service_instances
SET num_replicas = '2',
start_command = 'npm run start',
healthcheck_path = '/healthz'
WHERE service_id = '<service-id>'
AND environment_id = '<environment-id>';

Deployment operations are EXEC methods. The SHOWRESULTS hint returns the API's answer, including a refusal:

EXEC /*+ SHOWRESULTS */ railway.services.service_instances.redeploy
@service_id = '<service-id>',
@environment_id = '<environment-id>';

EXEC /*+ SHOWRESULTS */ railway.deployments.deployments.restart
@id = '<deployment-id>';

EXEC /*+ SHOWRESULTS */ railway.deployments.deployments.rollback
@id = '<deployment-id>';

Preview environment cleanup​

Pull request environments accumulate. The ephemeral environments of a project, with the pull request each one belongs to:

SELECT id, name, created_at,
json_extract(meta, '$.pr_number') AS pr_number,
json_extract(meta, '$.branch') AS branch
FROM railway.environments.environments
WHERE project_id = '<project-id>'
AND is_ephemeral = true
ORDER BY created_at;

Removing one is a DELETE. Running the listing again confirms it is gone:

DELETE FROM railway.environments.environments
WHERE id = '<environment-id>';

A join across providers​

Services deployed from a GitHub repository can be matched to the repository itself, for example to find services still deploying from a repository that has been archived or has not been pushed to in some time. This uses the github provider alongside railway:

SELECT s.service_name,
r.full_name AS repo, r.archived, r.pushed_at
FROM railway.services.service_instances s
JOIN github.repos.repos r
ON r.full_name = json_extract(s.source, '$.repo')
WHERE s.environment_id = '<environment-id>'
AND r.org = 'my-org'
ORDER BY r.pushed_at;

Get started​

Pull the provider from the public registry:

registry pull railway;

Provider docs are at railway-provider.stackql.io. Let us know what you build. Star us on GitHub.

New GitLab Provider Available

· 6 min read
Technologist and Cloud Consultant

We've released a new StackQL provider for GitLab:

  • gitlab - the GitLab GraphQL API as a read-only SQL surface: projects, groups, users, issues, merge_requests, ci, work_items, security, packages, snippets, boards, analytics, audit, metadata, admin, workspaces, ml and duo (18 services, 244 resources, every one of them SELECT)

The provider is generated from the introspection schema gitlab.com publishes, pinned by content hash and refreshed as a reviewed diff. It works against gitlab.com out of the box and routes to a self-managed instance from an environment variable. It is read-only by architecture: StackQL's GraphQL path is a query path, so there are no INSERT, UPDATE, DELETE or EXEC methods. What it is for is inventory, reporting and cross-provider joins over the GitLab control plane - the questions that otherwise need a script and three API clients.

Connect​

Authentication is a personal access token with the read_api scope, read from GITLAB_TOKEN - the same variable the Terraform GitLab provider uses:

export GITLAB_TOKEN=glpat-...
stackql shell

Public projects and groups on gitlab.com are readable without a token (--auth='{"gitlab": {"type": "null_auth"}}'). For a self-managed instance, set GITLAB_HOST=gitlab.example.com and every query routes there; an explicit WHERE host = ... still wins for addressing another instance in the same session.

Three scopes, one shape​

Resources follow the GraphQL schema. Instance-scoped resources take optional filters only (projects, users, runners); project-scoped resources are prefixed project_ and take the project path; group-scoped resources are prefixed group_ and take the group path. Columns are snake_case, and the nested identity objects GitLab attaches everywhere (author, namespace, milestone, user) are JSON columns one json_extract away.

Every project in a group tree, with activity and visibility signals:

SELECT full_path, visibility, archived,
star_count, forks_count,
open_issues_count, open_merge_requests_count,
last_activity_at
FROM gitlab.groups.group_projects
WHERE full_path = 'gitlab-org' AND include_subgroups = true
ORDER BY last_activity_at DESC;

GitLab connections are Relay-paginated with a hard page size of 100. StackQL walks the pageInfo cursor chain transparently, so the query above returns the whole tree, not the first page.

Filters are pushed down​

Every scalar or enum argument a GitLab field accepts is a WHERE parameter rendered into the GraphQL query itself, so the API does the filtering. Open merge requests with their approval state and age:

SELECT iid, title,
json_extract(author, '$.username') AS author,
draft, approved, approvals_left, detailed_merge_status,
ROUND(julianday('now') - julianday(created_at)) AS age_days
FROM gitlab.merge_requests.project_merge_requests
WHERE full_path = 'gitlab-org/gitlab-runner' AND state = 'opened'
ORDER BY age_days DESC;

Cycle time over a window (merged_after is a filter, merged_at a column):

SELECT iid, title,
ROUND((julianday(merged_at) - julianday(created_at)) * 24, 1) AS hours_to_merge
FROM gitlab.merge_requests.project_merge_requests
WHERE full_path = 'gitlab-org/gitlab-runner'
AND state = 'merged'
AND merged_after = '2026-09-01T00:00:00Z'
ORDER BY merged_at DESC;

Pipelines and runners​

Pipeline outcomes for a project, and the same across every project in a group by joining the inventory to each project's pipelines (the engine issues one pipelines request per project):

SELECT status, count(*) AS pipelines, ROUND(AVG(duration) / 60.0, 1) AS avg_minutes
FROM gitlab.ci.project_pipelines
WHERE full_path = 'gitlab-org/gitlab-runner'
GROUP BY status;

SELECT p.full_path,
count(*) AS pipelines,
SUM(c.status = 'FAILED') AS failed,
ROUND(100.0 * SUM(c.status = 'FAILED') / count(*), 1) AS failure_pct
FROM gitlab.groups.group_projects p
JOIN gitlab.ci.project_pipelines c ON c.full_path = p.full_path
WHERE p.full_path = 'my-group'
GROUP BY p.full_path
ORDER BY failure_pct DESC;

Runner fleet status for a group, with contact recency (the instance-wide runner listing is administrator-only on gitlab.com; the group and project listings are what a token can read):

SELECT id, description, runner_type, status, paused, contacted_at, upgrade_status
FROM gitlab.ci.group_runners
WHERE full_path = 'my-group'
ORDER BY contacted_at DESC;

Vulnerability reporting​

Findings across a group by severity and state, then the unresolved criticals with their owning project:

SELECT severity, state, count(*) AS findings
FROM gitlab.security.group_vulnerabilities
WHERE full_path = 'my-group'
GROUP BY severity, state;

SELECT json_extract(project, '$.full_path') AS project,
title, report_type, detected_at, web_url
FROM gitlab.security.group_vulnerabilities
WHERE full_path = 'my-group' AND severity = 'CRITICAL' AND state = 'DETECTED'
ORDER BY detected_at;

Membership, and a join across providers​

Group membership with access level is a table:

SELECT json_extract(user, '$.username') AS username,
json_extract(user, '$.name') AS name,
json_extract(access_level, '$.string_value') AS access_level,
expires_at
FROM gitlab.groups.group_group_members
WHERE full_path = 'my-group';

which makes the offboarding question a LEFT JOIN against the identity provider - GitLab members with no active Okta user, computed locally by the SQL engine after registry pull okta:

SELECT json_extract(m.user, '$.username') AS gitlab_username,
json_extract(m.user, '$.name') AS gitlab_name,
o.status AS okta_status
FROM gitlab.groups.group_group_members m
LEFT JOIN okta.user.users o
ON lower(json_extract(o.profile, '$.login')) = lower(json_extract(m.user, '$.username') || '@example.com')
AND o.subdomain = 'my-okta-org'
WHERE m.full_path = 'my-group'
AND (o.status IS NULL OR o.status != 'ACTIVE');

How it is built​

  • The selection set for every resource is generated by one policy from the schema: all scalar and enum fields of the node type plus a fixed allowlist of nested identity objects. The query text and the response schema come from the same field list, so DESCRIBE always matches what the wire returns.
  • GitLab enforces a query complexity limit (200 anonymous, 250 authenticated on gitlab.com). Every generated query is scored against the live limit as a build gate; the one node type that exceeded it (merge requests, at 245) was trimmed by measured per-field cost, protecting the approval and merge-state columns, to 190.
  • A handful of resolvers time out on large result sets or answer anonymous callers with a server error. Those fields are excluded by a recorded policy entry rather than left to fail whole pages.
  • all_services.csv in the repository records which schema field backs every resource and method, and regeneration fails when a mapping moves, so resource names stay stable between provider versions.

Premium-tier fields (issue weight, health status, epics) read back as null on the free tier, and the schema is gitlab.com's, so an older self-managed instance may reject fields it does not serve.

Get started​

Pull the provider from the public registry:

registry pull gitlab;

Provider docs are at gitlab-provider.stackql.io. Let us know what you build. Star us on GitHub.

Anthropic Providers Update - September 2026

· 4 min read
Technologist and Cloud Consultant

We've released an update to the StackQL anthropic provider, regenerated from the current Claude API specification. The provider now covers 12 services, 27 resources and 108 operations (up from 11, 26 and 103 in the July release). Changes in this release:

  • A new dreams service
  • Files and skills moved to their generally available endpoints
  • Workspace-scoped queries on most operations through the anthropic-workspace-id parameter

The anthropic_admin provider (6 services, 11 resources, 27 operations) is unchanged in this release.

Dreams​

Dreams are asynchronous memory-consolidation jobs: a dream reads a memory store and a set of session transcripts and writes consolidated memories into an output store. The service is a research preview on the Anthropic side, so the endpoints return a 404 for keys that are not enrolled. The dreams resource maps the surface as follows:

MethodSQL verbOperation
list, getSELECTlist dreams (cursor-paginated, walked automatically), get a dream
createINSERTstart a dream over one or more inputs
cancel, archiveEXEClifecycle operations

Dreams and their status as rows:

SELECT id, status, JSON_ARRAY_LENGTH(inputs) AS input_count, created_at, ended_at
FROM anthropic.dreams.dreams
ORDER BY created_at DESC;

Starting one is an INSERT. inputs and model are JSON values that the provider passes through as structured request fields:

INSERT INTO anthropic.dreams.dreams (inputs, model, instructions)
SELECT '[{"type": "memory_store", "memory_store_id": "memstore_01..."}]',
'{"id": "claude-opus-5"}',
'Consolidate project decisions and open questions.'
RETURNING id, status;

Files and skills on GA endpoints​

The Files API and the Skills API are generally available. The files and skills services now call the GA endpoints (/v1/files, /v1/skills) instead of the beta ones, and no longer send an anthropic-beta header. Resources, methods and SQL verbs are unchanged, so existing queries keep working. One behavioural change: the files list is now cursor-paginated and the provider walks the pages automatically.

SELECT id, filename, mime_type, size_bytes, created_at
FROM anthropic.files.files
ORDER BY created_at DESC;

SELECT id, display_name, JSON_EXTRACT(source, '$.type') AS source, latest_version_id, updated_at
FROM anthropic.skills.skills
ORDER BY updated_at DESC;

Skill versions are a separate resource keyed by the skill:

SELECT id, skill_id, name, description, created_at
FROM anthropic.skills.versions
WHERE skill_id = 'xlsx';

Uploading a file or creating a skill version is a multipart request, which SQL cannot express; those two methods remain documented as EXEC operations, and the rest of each resource (list, get, delete) is plain SQL.

Workspace-scoped queries​

The Claude API added an optional anthropic-workspace-id header to most operations, for credentials that can act on more than one workspace. The provider exposes it as an optional parameter on around 110 operations. It is a hyphenated name, so it is double-quoted in SQL:

SELECT id, display_name, created_at
FROM anthropic.models.models
WHERE "anthropic-workspace-id" = 'wrkspc_01CZkZaBF1tNoB5wlCeusgy';

The workspace ids are the ones the anthropic_admin provider lists:

SELECT id, name, archived_at
FROM anthropic_admin.workspaces.workspaces;

A key that belongs to a single workspace can omit the parameter. The API validates the value: a malformed id is rejected with a 400.

Under the hood​

The provider is generated from the OpenAPI specification that Anthropic now bundles with its SDKs. That specification grew from 126 to 244 operations since July, and the SDKs' configured endpoint count from 116 to 201. Beyond the changes above, the growth is the beta twins of the GA files and skills endpoints and the Admin API, which the specification now models but which needs an org-scoped admin key and belongs to the anthropic_admin provider. Every operation in the specification is either mapped or listed in a documented exclusion list, and the build fails when the two do not add up.

Every documented example query runs in the provider's test suite, and each generated docs page now shows the date it was last regenerated.

Authentication​

Unchanged. The two providers use different key types, which are disjoint by design:

# anthropic - workspace-scoped Claude API key
export ANTHROPIC_API_KEY=sk-ant-api...

# anthropic_admin - org-scoped Admin API key (created by org admins)
export ANTHROPIC_ADMIN_KEY=sk-ant-admin...

Get started​

Pull the latest provider from the public registry:

registry pull anthropic;

Provider docs, including required parameters and example queries for every resource, are at anthropic-provider.stackql.io and anthropic-admin-provider.stackql.io. Visit us on GitHub and let us know how you're using it.

Datadog Provider - August 2026

· 6 min read
Technologist and Cloud Consultant

We've released an updated StackQL Datadog provider covering the Datadog v1 and v2 REST APIs together: 18 services, 597 resources and 1658 operations, up from 16 services and 575 operations in the previous release.

What's new​

The previous provider was built from the v2 API alone. Datadog's most-used resources - monitors, dashboards, synthetics, SLOs, hosts, log indexes and pipelines - only exist in the v1 API, so this release merges the two specs into one provider. The v2 surface has also grown considerably since the last build. In summary:

  • The v1 API: monitors (list, search, create, replace, validate, delete), dashboards and dashboard lists, synthetics tests (API, browser and mobile), locations, private locations and global variables, SLOs and SLO corrections, hosts, host totals and host tags, notebooks, log indexes and pipelines, the Azure, PagerDuty, Slack and webhook integrations, and usage metering.
  • New v2 surfaces: cases and case projects, on-call schedules, escalation policies and paging, status pages, incident configuration and responders, feature flags, deployment gates, LLM Observability (projects, datasets, experiments, prompts, annotation queues), Fleet Automation, cloud cost budgets, commitments and tag pipelines, security findings automation, static analysis and SCA, agentless scanning, SIEM historical detections, RUM replay and product analytics, reference tables, org groups and personal access tokens, among others.
  • Site from the environment: the provider addresses https://api.{site}, and site is resolved from DD_SITE when it is set (datadoghq.eu, us5.datadoghq.com, ap2.datadoghq.com, ...), the same convention as the Datadog Agent and API clients. Queries carry no site clause; a WHERE site = '...' still wins for one statement.
  • Pagination and pushdown: cursor-paginated lists (audit events, container images, spans, RUM events, CI events, security signals and findings) are traversed transparently, and a SQL LIMIT is sent as the API's page-size parameter.
  • snake_case surface: columns and WHERE / INSERT keys are snake_case throughout; the few camelCase wire names are aliased.
  • Terraform-aligned authentication: DD_API_KEY and DD_APP_KEY, unchanged.

Service highlights​

ServiceResourcesOperationsWhat it covers
service_management82281incidents, cases, on-call, SLOs, downtimes, events, status pages, change management, error tracking
security93247security monitoring rules, signals and suppressions, findings and automation, vulnerabilities, CSM, agentless scanning, static analysis, SIEM historical detections
organization85207users, roles, permissions, API and application keys, service accounts, teams, org settings, SAML, audit logs, usage
integrations59192AWS, GCP, Azure, OCI, Jira, ServiceNow, Slack, Microsoft Teams, Google Chat, PagerDuty, Opsgenie, webhooks, Cloudflare, Confluent, Fastly, Okta, reference tables
monitoring39100monitors, synthetics, monitor policies, notification rules, service checks
digital_experience3898RUM applications, events, metrics and retention, replay, product analytics, sourcemaps
llm_observability3783projects, datasets, experiments, prompts, annotation queues, evaluators, Model Lab
cloud_costs3773budgets, AWS / Azure / GCP / OCI cost configs, commitments, tag pipelines, cost attribution
software_delivery2071CI pipelines and tests, DORA, deployment gates, workflows, feature flags, code coverage
dashboards1661dashboards, dashboard lists, powerpacks, notebooks, widgets, annotations, scheduled reports
logs1455indexes, pipelines, archives, custom destinations, log metrics, restriction queries, observability pipelines
infrastructure2847hosts and host tags, containers, processes, network devices, app builder, storage management
metrics1842metrics and metadata, tag configurations, timeseries and scalar queries, datasets, DDSQL
apm1027retention filters, spans metrics, scorecards, traces
remote_config627CSM Threats agent rules and policies, WAF rules and policies
actions623action connections, datastores, execution policies
fleet616agents, deployments, schedules, tracers
catalog38software catalog entities, kinds, relations

Authentication​

Export an API key and an application key; set DD_SITE if your organization is not on datadoghq.com:

export DD_API_KEY=...
export DD_APP_KEY=...
export DD_SITE=datadoghq.eu # optional, defaults to datadoghq.com

Monitors​

Every monitor with its state:

SELECT id, name, type, overall_state, tags
FROM datadog.monitoring.monitors;

Only alerting monitors, using the API's own filter:

SELECT id, name, overall_state
FROM datadog.monitoring.monitors
WHERE group_states = 'alert';

Monitor search, with the same syntax as the Manage Monitors page:

SELECT id, name, status, type
FROM datadog.monitoring.monitor_search_results
WHERE query = 'type:metric status:alert';

Dashboards, SLOs and synthetics​

SELECT id, title, layout_type, author_handle, modified_at
FROM datadog.dashboards.dashboards;

SELECT id, name, type, target_threshold, timeframe
FROM datadog.service_management.slos;

SELECT public_id, name, type, status, locations
FROM datadog.monitoring.synthetics_tests;

Users, roles and keys​

v2 resources return the JSON:API row shape - id, type, attributes, relationships - so attributes are one json_extract away. A user audit:

SELECT id,
json_extract(attributes, '$.email') AS email,
json_extract(attributes, '$.status') AS status,
json_extract(attributes, '$.disabled') AS disabled,
json_extract(attributes, '$.created_at') AS created_at
FROM datadog.organization.users;

API keys by age, the input to a rotation policy:

SELECT id,
json_extract(attributes, '$.name') AS name,
json_extract(attributes, '$.created_at') AS created_at,
json_extract(attributes, '$.last4') AS last4
FROM datadog.organization.api_keys
ORDER BY created_at;

Infrastructure and logs​

Hosts reporting to Datadog, and the log indexes with their retention:

SELECT host_name, up, is_muted, apps, last_reported_time
FROM datadog.infrastructure.hosts;

SELECT name, num_retention_days, daily_limit
FROM datadog.logs.indexes;

Audit log​

The audit event list is cursor-paginated and takes the time window as a query parameter:

SELECT json_extract(attributes, '$.timestamp') AS timestamp,
json_extract(attributes, '$.attributes.evt.name') AS event,
json_extract(attributes, '$.attributes.usr.email') AS actor
FROM datadog.organization.audit_logs
WHERE "filter[from]" = 'now-1d';

Security monitoring rules​

Which detection rules are enabled, and who last changed them:

SELECT id, name, type, is_enabled, is_default, updated_at, update_author_id
FROM datadog.security.monitoring_rules
WHERE is_default = false;

Provisioning​

Mutations use the same SQL grammar. v1 resources take their fields as columns; v2 resources take the JSON:API data document. A monitor end to end - validate the definition, create it, replace it (the v1 monitor API updates with PUT), delete it:

EXEC datadog.monitoring.monitors.validate_monitor
@type = 'metric alert',
@query = 'avg(last_5m):avg:system.cpu.user{env:prod} by {host} > 90',
@name = 'High CPU on prod hosts';

INSERT INTO datadog.monitoring.monitors (name, type, query, message, tags)
SELECT 'High CPU on prod hosts',
'metric alert',
'avg(last_5m):avg:system.cpu.user{env:prod} by {host} > 90',
'CPU above 90% on {{host.name}} @slack-ops',
'["team:web", "managed-by:stackql"]';

REPLACE datadog.monitoring.monitors
SET name = 'High CPU on prod hosts', type = 'metric alert',
query = 'avg(last_5m):avg:system.cpu.user{env:prod} by {host} > 95'
WHERE monitor_id = 12345678;

DELETE FROM datadog.monitoring.monitors
WHERE monitor_id = 12345678;

A downtime for a release window, and a role:

INSERT INTO datadog.service_management.downtimes (data)
SELECT '{"type": "downtime",
"attributes": {"message": "release window", "scope": "env:prod",
"monitor_identifier": {"monitor_tags": ["team:web"]},
"schedule": {"start": "2026-09-01T22:00:00Z", "end": "2026-09-01T23:00:00Z"}}}';

INSERT INTO datadog.organization.roles (data)
SELECT '{"type": "roles", "attributes": {"name": "read-only-auditors"}}';

UPDATE datadog.organization.roles
SET data = '{"id": "<role-id>", "type": "roles", "attributes": {"name": "auditors"}}'
WHERE role_id = '<role-id>';

Get started​

Pull the provider from the public registry:

registry pull datadog;

Provider docs are at datadog-provider.stackql.io. Let us know what you build. Star us on GitHub.

New Kubernetes Provider Available

· 6 min read
Technologist and Cloud Consultant

We've rebuilt the StackQL Kubernetes provider from the ground up:

  • k8s - every built-in control plane API group in a pinned Kubernetes minor release (currently 1.36): core, apps, batch, autoscaling, networking, storage, rbac, policy, apiextensions, admissionregistration, certificates, coordination, discovery, events, flowcontrol, node, scheduling, authentication, authorization and apiregistration (20 services, 152 resources, 573 operations)

The provider is generated from the per-group specs published in the Kubernetes repository, so the same provider works against kind, EKS, GKE, AKS, OpenShift or bare metal. Subresources (status, scale, log, eviction, binding, approval) are first-class resources, list pagination is traversed transparently, and LIMIT and label/field selectors are pushed down to the API server.

Connect with kubectl proxy​

The provider defaults to null_auth, designed for the kubectl proxy workflow - the proxy authenticates with your kubeconfig (including the EKS, GKE and AKS credential plugins), and StackQL connects to the local port with no configuration:

kubectl proxy --port=8001
export KUBE_HOST='localhost:8001'
export KUBE_PROTOCOL='http'
stackql shell

KUBE_HOST and KUBE_PROTOCOL resolve the provider's server variables from the environment, so queries carry no connection clauses at all (an explicit WHERE protocol = ... AND cluster_addr = ... still wins when you want to address another cluster in the same session).

The cluster is a database​

Row columns are the top-level fields of each object (metadata, spec, status, data); nested values are one json_extract away. The pod estate with phase and node placement:

SELECT json_extract(metadata, '$.namespace') AS namespace,
json_extract(metadata, '$.name') AS name,
json_extract(status, '$.phase') AS phase,
json_extract(spec, '$.nodeName') AS node
FROM k8s.core.pods_all_namespaces;

Pods that are not running - a one-line cluster health check:

SELECT json_extract(metadata, '$.namespace') AS namespace,
json_extract(metadata, '$.name') AS name,
json_extract(status, '$.phase') AS phase
FROM k8s.core.pods_all_namespaces
WHERE json_extract(status, '$.phase') NOT IN ('Running', 'Succeeded');

Desired versus ready replicas for every deployment in a namespace:

SELECT json_extract(metadata, '$.name') AS name,
json_extract(spec, '$.replicas') AS want,
json_extract(status, '$.readyReplicas') AS ready
FROM k8s.apps.deployments
WHERE namespace = 'default';

Node inventory with kubelet version and schedulability:

SELECT json_extract(metadata, '$.name') AS name,
json_extract(status, '$.nodeInfo.kubeletVersion') AS kubelet,
json_extract(status, '$.nodeInfo.osImage') AS os,
json_extract(spec, '$.unschedulable') AS cordoned
FROM k8s.core.nodes;

RBAC audit and warning events​

Column names are snake_case at the SQL surface (role_ref, string_data, api_version), mapped to the API's camelCase on the wire - the same convention as the aws and azure providers. Who is bound to cluster-admin:

SELECT json_extract(metadata, '$.name') AS binding,
subjects
FROM k8s.rbac.cluster_role_bindings
WHERE json_extract(role_ref, '$.name') = 'cluster-admin';

Recent warning events across the cluster, usually the first place to look when something is off:

SELECT json_extract(metadata, '$.namespace') AS namespace,
reason,
message
FROM k8s.core.events_all_namespaces
WHERE type = 'Warning';

Server-side filtering and pagination​

Label and field selectors are ordinary WHERE parameters, pushed to the API server so the filtering happens where the data lives:

SELECT json_extract(metadata, '$.name') AS name
FROM k8s.core.pods_all_namespaces
WHERE label_selector = 'k8s-app=kube-dns';

SELECT ... LIMIT n lands on the wire as the Kubernetes limit parameter, and the continue token chain is followed transparently - a SELECT returns all rows even when the API server caps page sizes.

Provision, mutate and tear down​

Mutations are the usual SQL verbs - INSERT creates an object, UPDATE applies a merge patch, REPLACE is a full update and DELETE removes it. Body columns are the native wire property names:

-- create
INSERT INTO k8s.core.config_maps(namespace, metadata, data)
SELECT 'default', '{"name": "app-config"}', '{"greeting": "hello"}';

-- partial update (merge patch)
UPDATE k8s.core.config_maps
SET data = '{"mood": "optimistic"}'
WHERE namespace = 'default' AND name = 'app-config';

-- remove it
DELETE FROM k8s.core.config_maps
WHERE namespace = 'default' AND name = 'app-config';

Subresources are resources, so scaling a deployment is an UPDATE on its scale subresource:

UPDATE k8s.apps.deployments_scale
SET spec = '{"replicas": 3}'
WHERE namespace = 'default' AND name = 'web';

and point-in-time pod logs are a queryable column:

SELECT log FROM k8s.core.pods_log
WHERE namespace = 'default' AND name = 'web-6d5f9c7b8-x2x9k';

Streaming operations (exec, attach, port-forward, watch) use protocol upgrades and are out of scope for the generated provider.

Who am I, and can I​

The authentication and authorization review kinds are mapped too. The zero-parameter self review is a plain SELECT; the parameterized access reviews are an INSERT whose verdict comes back with RETURNING:

-- who am I
SELECT json_extract(status, '$.userInfo.username') AS username
FROM k8s.authentication.self_subject_reviews;

-- can I delete pods in prod
INSERT INTO k8s.authorization.self_subject_access_reviews(spec)
SELECT '{"resourceAttributes": {"verb": "delete", "resource": "pods", "namespace": "prod"}}'
RETURNING status;

What changed from the original provider​

This is a major update to the previous published k8s provider (v23.03.00121):

  • Coverage expands from 5 services to all 20 built-in API groups, one flat service per group (networking.k8s.io is k8s.networking)
  • Resource names are plural snake_case (k8s.core.pods, k8s.apps.stateful_sets), consistent with the aws, google and databricks providers
  • Request body columns are the native wire property names (metadata, spec, data), not data__ prefixed; snake_case spellings of camelCase wire names are accepted everywhere
  • SELECT and DESCRIBE columns present as snake_case aliases of the camelCase wire properties
  • Namespaced list-all operations are separate _all_namespaces resources, and subresources are separate resources (deployments_scale, pods_log)

The previous provider version remains in the registry for pinning if you need it.

Direct authentication​

To skip the proxy and hit the API server directly, supply a bearer token via the KUBE_TOKEN environment variable (the same variable the Terraform Kubernetes provider uses), along with the cluster CA bundle:

export KUBE_TOKEN=$(kubectl create token my-serviceaccount)
AUTH='{ "k8s": { "type": "bearer", "credentialsenvvar": "KUBE_TOKEN" }}'
stackql shell --auth="${AUTH}" --tls.CABundle cluster-ca.pem

For managed clusters, a token from the platform credential helper (aws eks get-token, gke-gcloud-auth-plugin, kubelogin) works the same way; those tokens are short lived, so prefer the proxy vector for long sessions.

Get started​

Pull the provider from the public registry:

registry pull k8s;

Provider docs are at k8s-provider.stackql.io. Let us know what you build. Star us on GitHub.