Create Private Workflow Templates with Custom Blueprints

For the complete documentation index, see llms.txt. For a full content snapshot, see llms-full.txt. Append .md to any kestra.io/docs/* URL for plain Markdown.

Available on:Enterprise EditionCloud

Custom Blueprints are private, reusable workflow templates that extend the publicly available Community Blueprints.

Custom Blueprints help centralize orchestration patterns, document best practices, and streamline collaboration across your organization.

Creating a custom blueprint

From the left navigation menu, go to Blueprints, select the Custom Blueprints tab, and click Create. Add a title, description, and the flow YAML, then click Create.

You can edit Blueprints at any time, for example, to add new tasks or expand the documentation.

Templated Blueprints

Templated Blueprints allow you to create reusable, configurable workflows that users can instantiate without editing YAML. Instead of copying and modifying Blueprints, users fill in guided inputs and Kestra generates the complete flow automatically.

Platform teams build templates once; business users instantiate them by filling in a form rather than editing YAML.

How it works: Templated Blueprints use Pebble templating, with custom delimiters to avoid conflicts with Kestra expressions.

Define Template Arguments

Template arguments define the inputs users must provide. To add them to your Blueprint, use the extend key with a templateArguments section:

extend:
templateArguments:
- id: values
displayName: An array of values
type: MULTISELECT
values:
- value1
- value2
- value3

All Kestra input types and their validation rules are supported. These arguments automatically generate a UI form when the blueprint is instantiated.

Conditional Template Arguments

A template argument can depend on other arguments with dependsOn, the same way as conditional inputs. The form shows it only once its condition is true. The condition reads the other arguments through arg (or inputs), and can be written with << >> or {{ }}.

extend:
templateArguments:
- id: target
type: SELECT
values:
- Postgres
- Oracle
- id: oracle_sid
displayName: Oracle SID
type: STRING
required: true
dependsOn:
inputs:
- target
condition: "<< arg.target == 'Oracle' >>"

Here oracle_sid is asked for, and required, only when target is Oracle. When the condition is false, the argument is not required and << arg.oracle_sid >> renders as an empty string.

Use Template Arguments

Templated blueprints use the Pebble templating engine. To avoid conflicts with Kestra expressions ({{ }}), template arguments use custom delimiters: << and >>.

Template arguments are accessed using the arg prefix. For example, if you have a template argument with id: my_custom_field, you can use it in your flow as follows:

tasks:
- id: log
type: io.kestra.plugin.core.log.Log
message: Hello << arg.my_custom_field >>

Loops and Conditions

You can dynamically generate multiple tasks, inputs, variables, or triggers through for-loops and if/else conditions using the <% %> syntax.

For example, the following loop creates one log task for each value in an array input.

extend:
templateArguments:
- id: values
displayName: An array of values
type: MULTISELECT
values:
- value1
- value2
- value3
id: myflow
namespace: company.team
tasks:
<% for value in arg.values %>
- id: log_<< value >>
type: io.kestra.plugin.core.log.Log
message: Hello << value >>
<% endfor %>

This allows you to dynamically generate tasks or include them conditionally. A line that holds only <% %> tags is removed from the generated flow, so loops and conditions leave no blank lines behind.

Comments and Kestra Expressions

SyntaxRendered when the blueprint is used
<< ... >>Prints a value, such as << arg.env >>
<% ... %>Runs a tag, such as for or if
<# ... #>A comment, removed from the generated flow

Kestra expressions such as {{ outputs.download.uri }} or {{ secret('API_KEY') }}, and {% %} tags, are copied into the generated flow unchanged and evaluated when the flow runs.

These functions are not available in template expressions: kv(), http(), assets(), printContext(), fetchContext(), errorLogs(), tasksWithState() and currentEachOutput(). Using one fails with Function or Macro [kv] does not exist. Call them in a {{ }} expression instead, which is evaluated when the flow runs.

Example: Data Ingestion Template

Here’s an example showing a Templated Blueprint that generates data ingestion workflows based on user selections:

Template Definition
id: data-ingest
namespace: kestra.data
extend:
templateArguments:
- id: domains
displayName: Domains
type: MULTISELECT
values:
- Sales
- Manufacture
- HR
- Finance
- id: target
type: SELECT
values:
- Postgres
- Oracle
- id: env
type: SELECT
values:
- dev
- staging
- prod
tasks:
- id: parallel_<< arg.env >>
type: io.kestra.plugin.core.flow.Parallel
tasks:
<% for domain in arg.domains %>
- id: sequential_<< domain | slugify >>
type: io.kestra.plugin.core.flow.Sequential
tasks:
- id: << domain | slugify >>_download
type: io.kestra.plugin.jdbc.postgresql.Query
url: jdbc:postgresql://sample_postgres:5432/<< arg.env >>
username: "{{ secret('POSTGRES_USERNAME') }}"
password: "{{ secret('POSTGRES_PASSWORD') }}"
sql: SELECT * FROM public.<< domain | slugify >>
fetchType: STORE
- id: << domain | slugify >>_ingest
<% if arg.target == 'Oracle' %>
type: io.kestra.plugin.jdbc.oracle.Batch
url: jdbc:oracle:thin:@<< arg.env >>:49161:XE
username: "{{ secret('ORACLE_USERNAME') }}"
password: "{{ secret('ORACLE_PASSWORD') }}"
<% elseif arg.target == 'Postgres' %>
type: io.kestra.plugin.jdbc.postgresql.Batch
url: jdbc:postgresql://warehouse_postgres:5432/<< arg.env >>
username: "{{ secret('POSTGRES_USERNAME') }}"
password: "{{ secret('POSTGRES_PASSWORD') }}"
<% endif %>
from: "{{ outputs.<< domain | slugify >>_download.uri }}"
table: << domain | slugify >>
<% endfor %>
Generated Flow (after template rendering)

After selecting env: dev, domains: [HR, Manufacture], and target: Oracle, the template generates this complete workflow:

id: data-ingest
namespace: kestra.data
tasks:
- id: parallel_dev
type: io.kestra.plugin.core.flow.Parallel
tasks:
- id: sequential_hr
type: io.kestra.plugin.core.flow.Sequential
tasks:
- id: hr_download
type: io.kestra.plugin.jdbc.postgresql.Query
url: jdbc:postgresql://sample_postgres:5432/dev
username: "{{ secret('POSTGRES_USERNAME') }}"
password: "{{ secret('POSTGRES_PASSWORD') }}"
sql: SELECT * FROM public.hr
fetchType: STORE
- id: hr_ingest
type: io.kestra.plugin.jdbc.oracle.Batch
url: jdbc:oracle:thin:@dev:49161:XE
username: "{{ secret('ORACLE_USERNAME') }}"
password: "{{ secret('ORACLE_PASSWORD') }}"
from: "{{ outputs.hr_download.uri }}"
table: hr
- id: sequential_manufacture
type: io.kestra.plugin.core.flow.Sequential
tasks:
- id: manufacture_download
type: io.kestra.plugin.jdbc.postgresql.Query
url: jdbc:postgresql://sample_postgres:5432/dev
username: "{{ secret('POSTGRES_USERNAME') }}"
password: "{{ secret('POSTGRES_PASSWORD') }}"
sql: SELECT * FROM public.manufacture
fetchType: STORE
- id: manufacture_ingest
type: io.kestra.plugin.jdbc.oracle.Batch
url: jdbc:oracle:thin:@dev:49161:XE
username: "{{ secret('ORACLE_USERNAME') }}"
password: "{{ secret('ORACLE_PASSWORD') }}"
from: "{{ outputs.manufacture_download.uri }}"
table: manufacture

slugify turns an argument value into a lowercase id, but a value with several words keeps a dash: Online Shop becomes online-shop. Reference the output of such a task with bracket notation, outputs['online-shop_download'].uri, since outputs.online-shop_download.uri is rejected when the flow is saved.

Version control for Custom Blueprints

Custom Blueprints can be version-controlled with Git using two dedicated tasks from the plugin-ee-git plugin:

  • PushBlueprints commits and pushes blueprints from Kestra to a Git repository.
  • SyncBlueprints syncs blueprints from a Git repository into Kestra, treating Git as the single source of truth.

These tasks mirror the PushFlows and SyncFlows patterns used for flows, applied to Custom Blueprints.

Push blueprints to Git

PushBlueprints exports blueprints from Kestra into a Git repository. This is suited to creating backups, reviewing changes via pull requests, or promoting blueprints across environments.

Each blueprint is written as a YAML file to the target gitDirectory (default: _blueprints). Use the blueprints property with glob patterns to push only a subset of blueprints.

id: push_blueprints
namespace: system
tasks:
- id: commit_and_push
type: io.kestra.plugin.ee.git.PushBlueprints
url: https://github.com/your-org/blueprints-repo
username: git_username
password: "{{ secret('GITHUB_ACCESS_TOKEN') }}"
branch: main
commitMessage: "push blueprints from {{ flow.namespace ~ '.' ~ flow.id }}"
triggers:
- id: schedule
type: io.kestra.plugin.core.trigger.Schedule
cron: "0 * * * *"

The task outputs a commitId, a commitURL, and a blueprints URI pointing to a diff report that lists the number of lines added, deleted, and changed per file.

Sync blueprints from Git

SyncBlueprints pulls blueprints from Git into Kestra, treating Git as the single source of truth. This pattern is suited to platform teams managing approved blueprint libraries centrally and deploying them across multiple Kestra instances.

By default, SyncBlueprints only adds and updates blueprints. Set delete: true to also remove any blueprints present in Kestra but absent in Git.

id: sync_blueprints_from_git
namespace: system
tasks:
- id: git
type: io.kestra.plugin.ee.git.SyncBlueprints
url: https://github.com/your-org/blueprints-repo
branch: main
username: git_username
password: "{{ secret('GITHUB_ACCESS_TOKEN') }}"
delete: true
dryRun: true
triggers:
- id: every_full_hour
type: io.kestra.plugin.core.trigger.Schedule
cron: "0 * * * *"

Set dryRun: true to preview what would change without applying it. The blueprints output URI contains a row-per-blueprint report showing each blueprint’s syncState: ADDED, UPDATED, UNCHANGED, or DELETED.

Use caution with delete: true: it removes all blueprints not present in Git, not just those that differ.

Blueprint YAML file format

Both tasks read and write blueprints as YAML files. Each file represents one blueprint:

id: my-blueprint-id
title: My Blueprint Title
description: Optional description of what this blueprint does
tags:
- tag1
- tag2
flow: |
id: my-flow
namespace: company.team
tasks:
- id: hello
type: io.kestra.plugin.core.log.Log
message: Hello World

The id field controls how blueprints are matched on sync: if a blueprint with that ID already exists in Kestra, it is updated; if not, it is created with that ID. If id is omitted, a new blueprint is created with an auto-generated ID.

Was this page helpful?