Summary
Azure HorizonDB is Microsoft's fully managed, cloud-native PostgreSQL service engineered for AI-ready, mission-critical workloads at scale, featuring a disaggregated compute-and-storage architecture and a built-in durable execution engine (pg_durable). This plugin adds Kestra tasks and triggers to run SQL queries against HorizonDB — including the pg_durable SQL DSL — so teams can submit, monitor, signal, and react to long-running durable workflows entirely from within a Kestra flow, without external orchestrators or custom Python glue scripts.
Motivation
Teams using Azure HorizonDB currently run durable workflows (ETL jobs, approval flows, scheduled maintenance) by cobbling together Azure Functions, cron jobs, or external orchestrators that must reach back into the database. With this plugin, every step — submitting a durable function, waiting for completion, sending an external signal, reacting to a state change — lives in the same Kestra flow as upstream data preparation, notifications, and downstream integrations. This removes the operational burden of a separate orchestration service for workloads whose work is primarily database-side.
Context
Azure HorizonDB is in public preview (announced at Build 2026) and is available in select Azure regions (Central US, West US 2, West US 3, Sweden Central, Australia East). The plugin should be added as a new sub-package io.kestra.plugin.azure.horizondb within plugin-azure, following the same structural conventions as io.kestra.plugin.azure.storage.cosmosdb and io.kestra.plugin.azure.monitoring. The existing plugin-jdbc (PostgreSQL) plugin can serve as a reference for JDBC-based query tasks; the durable.* tasks have no prior counterpart and should be implemented fresh.
Official documentation:
API Reference
- Official docs: https://learn.microsoft.com/en-us/azure/horizondb/
- Authentication: PostgreSQL password auth or Azure Entra ID (via
com.azure.identity.extensions.jdbc.postgresql.AzurePostgresqlAuthenticationPlugin passed as authenticationPluginClassName in the JDBC URL)
- Base URL pattern:
jdbc:postgresql://<host>:5432/<database>
- SDK / client library: Standard PostgreSQL JDBC driver — no proprietary HorizonDB SDK. Entra ID integration uses the Azure Identity Extensions library.
Gradle Dependencies
Add to build.gradle:
// PostgreSQL JDBC driver
implementation "org.postgresql:postgresql:42.7.5"
// Azure Identity JDBC extensions (Entra ID / Managed Identity auth for PostgreSQL)
implementation "com.azure:azure-identity-extensions:1.2.0"
Use the latest stable versions available on Maven Central.
Kestra framework inclusions (do not add as dependencies):
Kestra's internal HTTP client (io.kestra.core.http.client) and Jackson serializers are provided by the framework. Only list an explicit HTTP dependency when an official SDK is required — never list OkHttp, Apache HttpComponents, or java.net.http wrappers.
Plugin Structure
- Repository:
plugin-azure
- Namespace:
io.kestra.plugin.azure.horizondb
- Sub-plugins:
horizondb (SQL tasks), horizondb.durable (pg_durable workflow management)
- Categories:
CLOUD
(Each sub-package must have its own package-info.java annotated with @PluginSubGroup(category = PluginSubGroup.PluginCategory.CLOUD).)
Task class naming: task class names must not repeat the plugin or package name as a prefix or suffix. Use concise action or resource names: Query, Queries, Start, Cancel, Signal, GetStatus, ListInstances, Trigger — not HorizonDbQuery, DurableStart, etc.
Suggested Tasks
AbstractHorizonDb — abstract base class with shared connection properties (host, port, database, username, password, useEntraId); opens and closes a JDBC Connection; shared by all SQL and durable tasks.
Query — execute a single SQL statement; support fetchType (FETCH_ONE, FETCH, STORE) for result handling; emit row count and (optionally) an internal storage URI.
Queries — execute multiple SQL statements in a single connection; return a list of per-statement outputs.
durable.Start — submit a pg_durable function body via SELECT df.start(...) and return the instance ID.
durable.Cancel — cancel a running instance by ID via SELECT df.cancel(...).
durable.Signal — send an external signal to a waiting instance via SELECT df.signal(...).
durable.GetStatus — return the current status and result of a durable instance via df.status() / df.result().
durable.ListInstances — query df.list_instances() with optional status filter; support FETCH and STORE fetch types.
durable.Trigger — polling trigger that queries df.list_instances() on an interval and fires an execution when one or more instances reach a target state (e.g. Completed, Failed).
- Write unit tests (using Testcontainers with the
postgres image plus the pg_durable extension, if a public Docker image is available; otherwise use Wiremock to simulate JDBC responses).
- Add
package-info.java per sub-package with @PluginSubGroup(category = PluginSubGroup.PluginCategory.CLOUD).
- Add
metadata/horizondb.yaml and an io.kestra.plugin.azure.horizondb.svg icon.
- Add YAML examples and
@Plugin(examples = ...) entries; update AGENTS.md.
YAML Examples
Example 1 — Execute a SQL query against HorizonDB
id: horizondb_query
namespace: company.team
inputs:
- id: host
type: STRING
tasks:
- id: query
type: io.kestra.plugin.azure.horizondb.Query
host: "{{ inputs.host }}"
port: 5432
database: mydb
username: "{{ secret('HORIZONDB_USERNAME') }}"
password: "{{ secret('HORIZONDB_PASSWORD') }}"
sql: |
SELECT id, total, status FROM orders WHERE loaded_at > now() - INTERVAL '1 day'
fetchType: STORE
Example 2 — Submit a pg_durable ETL workflow and wait for it to complete
id: horizondb_durable_etl
namespace: company.team
inputs:
- id: host
type: STRING
tasks:
- id: start_etl
type: io.kestra.plugin.azure.horizondb.durable.Start
host: "{{ inputs.host }}"
port: 5432
database: mydb
username: "{{ secret('HORIZONDB_USERNAME') }}"
password: "{{ secret('HORIZONDB_PASSWORD') }}"
functionBody: |
'DELETE FROM target WHERE loaded_at < now() - INTERVAL ''1 day'''
~> 'INSERT INTO target SELECT * FROM staging'
~> 'REINDEX TABLE target'
~> 'INSERT INTO etl_log (job, finished_at) VALUES (''nightly'', now())'
label: nightly-etl
- id: poll_status
type: io.kestra.plugin.azure.horizondb.durable.GetStatus
host: "{{ inputs.host }}"
port: 5432
database: mydb
username: "{{ secret('HORIZONDB_USERNAME') }}"
password: "{{ secret('HORIZONDB_PASSWORD') }}"
instanceId: "{{ outputs.start_etl.instanceId }}"
Example 3 — Trigger a Kestra flow when a durable function instance completes
id: horizondb_durable_on_completion
namespace: company.team
triggers:
- id: on_durable_complete
type: io.kestra.plugin.azure.horizondb.durable.Trigger
host: "{{ secret('HORIZONDB_HOST') }}"
port: 5432
database: mydb
username: "{{ secret('HORIZONDB_USERNAME') }}"
password: "{{ secret('HORIZONDB_PASSWORD') }}"
targetStatus: Completed
interval: PT30S
tasks:
- id: log_completion
type: io.kestra.plugin.core.log.Log
message: "Durable instance {{ trigger.instanceId }} completed with status {{ trigger.status }}"
Acceptance Criteria
Functional
Kestra Plugin Coding Standards
Documentation & Structure
Repository Setup Checklist
Step 1 (Scaffold) is omitted — plugin-azure already exists.
1. Add to Sanity check page
Add this plugin to the Sanity check Notion page.
2. Run scoped Terraform apply
Run the following from infra/terraform/github:
terraform apply \
-target='github_repository.repo["plugin-azure"]' \
-target='github_issue_labels.plugins["plugin-azure"]' \
-target='github_repository_ruleset.branch["plugin-azure"]'
Summary
Azure HorizonDB is Microsoft's fully managed, cloud-native PostgreSQL service engineered for AI-ready, mission-critical workloads at scale, featuring a disaggregated compute-and-storage architecture and a built-in durable execution engine (
pg_durable). This plugin adds Kestra tasks and triggers to run SQL queries against HorizonDB — including thepg_durableSQL DSL — so teams can submit, monitor, signal, and react to long-running durable workflows entirely from within a Kestra flow, without external orchestrators or custom Python glue scripts.Motivation
Teams using Azure HorizonDB currently run durable workflows (ETL jobs, approval flows, scheduled maintenance) by cobbling together Azure Functions, cron jobs, or external orchestrators that must reach back into the database. With this plugin, every step — submitting a durable function, waiting for completion, sending an external signal, reacting to a state change — lives in the same Kestra flow as upstream data preparation, notifications, and downstream integrations. This removes the operational burden of a separate orchestration service for workloads whose work is primarily database-side.
Context
Azure HorizonDB is in public preview (announced at Build 2026) and is available in select Azure regions (Central US, West US 2, West US 3, Sweden Central, Australia East). The plugin should be added as a new sub-package
io.kestra.plugin.azure.horizondbwithinplugin-azure, following the same structural conventions asio.kestra.plugin.azure.storage.cosmosdbandio.kestra.plugin.azure.monitoring. The existingplugin-jdbc(PostgreSQL) plugin can serve as a reference for JDBC-based query tasks; thedurable.*tasks have no prior counterpart and should be implemented fresh.Official documentation:
pg_durable): https://learn.microsoft.com/en-us/azure/horizondb/development/durable-functionsAPI Reference
com.azure.identity.extensions.jdbc.postgresql.AzurePostgresqlAuthenticationPluginpassed asauthenticationPluginClassNamein the JDBC URL)jdbc:postgresql://<host>:5432/<database>Gradle Dependencies
Add to
build.gradle:Plugin Structure
plugin-azureio.kestra.plugin.azure.horizondbhorizondb(SQL tasks),horizondb.durable(pg_durable workflow management)CLOUD(Each sub-package must have its own
package-info.javaannotated with@PluginSubGroup(category = PluginSubGroup.PluginCategory.CLOUD).)Suggested Tasks
AbstractHorizonDb— abstract base class with shared connection properties (host,port,database,username,password,useEntraId); opens and closes a JDBCConnection; shared by all SQL and durable tasks.Query— execute a single SQL statement; supportfetchType(FETCH_ONE,FETCH,STORE) for result handling; emit row count and (optionally) an internal storage URI.Queries— execute multiple SQL statements in a single connection; return a list of per-statement outputs.durable.Start— submit apg_durablefunction body viaSELECT df.start(...)and return the instance ID.durable.Cancel— cancel a running instance by ID viaSELECT df.cancel(...).durable.Signal— send an external signal to a waiting instance viaSELECT df.signal(...).durable.GetStatus— return the current status and result of a durable instance viadf.status()/df.result().durable.ListInstances— querydf.list_instances()with optional status filter; supportFETCHandSTOREfetch types.durable.Trigger— polling trigger that queriesdf.list_instances()on an interval and fires an execution when one or more instances reach a target state (e.g.Completed,Failed).postgresimage plus thepg_durableextension, if a public Docker image is available; otherwise use Wiremock to simulate JDBC responses).package-info.javaper sub-package with@PluginSubGroup(category = PluginSubGroup.PluginCategory.CLOUD).metadata/horizondb.yamland anio.kestra.plugin.azure.horizondb.svgicon.@Plugin(examples = ...)entries; updateAGENTS.md.YAML Examples
Example 1 — Execute a SQL query against HorizonDB
Example 2 — Submit a pg_durable ETL workflow and wait for it to complete
Example 3 — Trigger a Kestra flow when a durable function instance completes
Acceptance Criteria
Functional
QueryandQueriestasks execute SQL and return results withfetchTypesupportdurable.Start,durable.Cancel,durable.Signal,durable.GetStatus,durable.ListInstancestasks implementeddurable.Triggerpolling trigger fires executions on instance state transitions./gradlew test)./gradlew build)Kestra Plugin Coding Standards
Property<T>— no legacy@PluginProperty(dynamic = true)on new codepasswordand any credential properties annotated with@PluginProperty(secret = true)@Schemaannotation@SuperBuilder,@ToString,@EqualsAndHashCode,@Getter,@NoArgsConstructor)runContext.logger()onlyio.kestra.core.serializersProperty<T>fields support Kestra expression languageDocumentation & Structure
@Plugin(examples = ...)entries each setfull = truewith a complete runnable flow (id + namespace + tasks/triggers){{ secret('SECRET_NAME') }}package-info.javaper sub-package with@PluginSubGroup(category = PluginSubGroup.PluginCategory.CLOUD)metadata/horizondb.yamlandio.kestra.plugin.azure.horizondb.svgicon presentAGENTS.mdupdated with new task class referencesRepository Setup Checklist
1. Add to Sanity check page
Add this plugin to the Sanity check Notion page.
2. Run scoped Terraform apply
Run the following from
infra/terraform/github:terraform apply \ -target='github_repository.repo["plugin-azure"]' \ -target='github_issue_labels.plugins["plugin-azure"]' \ -target='github_repository_ruleset.branch["plugin-azure"]'