Thanks for your interest in contributing. This document is the authoring guide for the Workflow Template Library that ships in Kibana (Tech Preview from 9.5). Read it before opening your first PR.
- Ways to contribute
- Authoring a template
- Validating locally
- Versioning
- Pull request flow
- Code of conduct
- Add a new template. Submit a new YAML under
library/workflows/<slug>/<slug>.yamlwith a validtemplate-metadatablock. - Improve an existing template. Tighten a description, fix a bug, swap a generic
httpstep for a dedicated vendor step type, add helpful install-form fields. - Extend the categories vocabulary. When a new template genuinely needs a category not in
library/categories.yaml, add the entry in the same PR. - Improve documentation. Fix unclear wording, add examples, clarify the authoring rules.
- Report issues. File a GitHub issue for bugs, suggestions, or missing capabilities.
Every template lives at:
library/workflows/<slug>/<slug>.yaml
- The
<slug>directory name and the YAML file name must match theslugvalue inside the file'stemplate-metadatablock. (Enforced by the validation step, planned to run in CI.) - Slug format: kebab-case, lowercase ASCII alphanumeric + hyphens. Should be descriptive and unique across the library.
- One template per directory. Future multi-version coexistence will live as
<slug>/<slug>-v<n>.yamlsiblings, but the starter set is all v1.
Top of every template file. The body that follows is regular workflow YAML grammar (consts:, inputs: / triggers:, steps:, …).
template-metadata:
slug: ip-reputation-check # MUST match parent dir name
version: "1.0.0" # semver; bump on every content change
availability: ">=9.5.0" # semver range over Kibana versions
name: "IP Reputation Check (AbuseIPDB)"
description: >-
Assess the reputation of an IP address using AbuseIPDB and enrich
with geolocation data. Produces a low / medium / high risk verdict.
solutions: [security] # optional. absent or empty = cross-solution (every solution context)
categories: [enrichment, threat-intel] # closed-vocab; entries MUST exist in library/categories.yaml
install: # only required when the body uses __install__.<name>
form:
- name: abuseipdb-connector
label: "AbuseIPDB connector"
description: "The AbuseIPDB connector used to query IP reputation."
inputType: connector
connectorType: .abuseipdb
required: trueRequired fields: slug, version, availability, name, description, categories.
Optional fields: solutions, install.
Notes on the optional fields:
solutions— when present, an array of solution ids (e.g.[security],[security, observability]). Absent or empty means the template is cross-solution and appears in every solution context.install— required if and only if the workflow body references any__install__.<name>placeholder. See Install-time inputs below.
categories: [...] is a closed vocabulary. Every value used in any template's categories array must exist as an id in library/categories.yaml. The validation step (planned to run in CI) rejects any template referencing an unknown id.
If your template genuinely needs a category that is not in the vocab, add the entry to library/categories.yaml in the same PR — never invent values used only in a template. Reviewers will either accept the new entry or point you at an existing one.
When the operator installs a template into their Kibana, the catalog UI renders an install form derived from template-metadata.install.form. Whatever values the user submits get substituted into the workflow body wherever it references __install__.<name>.
Three rules to internalize:
install.formis the single source of truth. Every__install__.<name>reference in the body MUST have a matching entry ininstall.form. The installer does not auto-derive form fields fromconsts:or anywhere else; an undeclared reference fails the install.- Form field names are kebab-case by convention (e.g.
abuseipdb-connector,max-age-in-days). They are internal template identifiers and are substituted away during rendering — end users never see them in the final workflow YAML. - References are plain text substitution — never wrap them in
{{ }}. See the next section; this is the most common authoring mistake.
Substitution is textual and happens once, at install time — before the workflow ever runs. The installer replaces every __install__.<name> occurrence in the body with the submitted value. Liquid ({{ ... }}) is a runtime templating layer evaluated on each execution against the run context (consts, inputs, steps.*); install fields do not exist in that context.
Therefore, never wrap an install reference in Liquid braces:
# ❌ WRONG — after install this becomes path: "/_ml/anomaly_detectors/{{ my-job }}/_forecast",
# and at runtime Liquid resolves `my-job` as a (nonexistent) variable → empty string.
path: "/_ml/anomaly_detectors/{{ __install__.job-id }}/_forecast"
# ✅ Bare reference — interpolated as text at install time.
path: "/_ml/anomaly_detectors/__install__.job-id/_forecast"Bare references work as a whole scalar (maxAgeInDays: __install__.max-age-in-days — the value keeps its type), inline inside longer strings, and inside |/> block scalars.
Recommended pattern for anything used more than once (or embedded in strings): assign the install value to a const, then use normal Liquid on the const. This gives the value a single, quote-safe injection point and keeps the body reading as ordinary workflow YAML:
consts:
job_id: __install__.job-id # snake_case! `{{ consts.job-id }}` would not parse in Liquid
steps:
- name: run_forecast
type: elasticsearch.request
with:
path: "/_ml/anomaly_detectors/{{ consts.job_id }}/_forecast"Exception: connector-id always references the install field directly (connector-id: __install__.slack-connector). It is resolved as a saved-object reference, not a Liquid-rendered value — this is the established pattern across all library templates.
What belongs in the install form (vs consts:):
- Promote to
install.formanything the operator must configure for the template to work: connector ids (always), Slack channels, recipient emails, tunable thresholds you want to expose in the install UX, environment-specific URLs. - Keep in
consts:stable, non-secret config that does not vary per installation: vendor base URLs (when the dedicated connector doesn't own them), hard-coded defaults the install UX does not need to expose.
Templates that don't need any install-time inputs omit the install: block entirely.
inputType |
Purpose | Required extras |
|---|---|---|
text |
Free-form short string | — |
textarea |
Multi-line text | — |
number |
Numeric input | — |
boolean |
Toggle | — |
select |
Single choice from a fixed list | options: [{ value, label }, ...] |
connector |
Picks an existing Kibana stack connector | connectorType: .<vendor> |
esIndex |
An Elasticsearch index name | — (renders as a plain text input for now; index autocomplete is planned) |
Every field can carry label, description, required (default false), and default.
install.form[].connectorType must equal . + the prefix of the step type that uses it. For example:
abuseipdb.checkIp→connectorType: .abuseipdbvirustotal.scanFileHash→connectorType: .virustotalslack2.createConversation→connectorType: .slack2brave-search.webSearch→connectorType: .brave-search
Never use .webhook as a connector type; always pick the dedicated .<vendor> connector.
The workflow engine's step-type registry is the source of truth — refer to the JSON schema published by @kbn/workflows for the canonical list.
Two rules:
- Prefer the dedicated vendor step type. If a vendor has a dedicated step (e.g.
abuseipdb.checkIp,virustotal.scanFileHash,slack2.createConversation,brave-search.webSearch), use it. The legacy generichttpstep is an escape hatch and should only appear when no dedicated step exists. - Never invent a step type or a connector type. If you think one is missing, file an issue rather than working around it locally.
The catalog generator derives stepTypes and triggerTypes for each template — the full type of every step (including nested steps) and every trigger — into the catalog row. The Library UI renders the step/trigger icons on each template card from these, which is why templates no longer carry a manual icon field.
- 2-space YAML indentation.
- No
id:, nometadata:(singular), nosince/discontinued/replacementfields. Those are obsolete shapes from earlier drafts. - Drop the legacy banner headers. No
# =================== Workflow: Xblock at the top, no# CONSTANTS / # INPUTS / # TRIGGERS / # STEPStutorial blocks. Thetemplate-metadatablock is the header. - Keep per-step comments, trimmed. One short paragraph per step explaining intent. Avoid restating what the YAML already says.
- Body comments must survive install on their own. At install time the entire
template-metadatablock — includinginstall.formand every field label/description — is stripped away; only the workflow body and its comments are rendered into the operator's installed workflow. So never write a body comment (or a runtimeconsolemessage) that refers totemplate-metadata, an install-form field, or its label/description — e.g. "(see install form)" or "enable the Overwrite existing option". Once installed, that thing no longer exists and the reference dangles. Instead, describe the value itself, and when you point the user at a knob to change, point at what survives into the installed YAML — theconsts:entry orinputs:value they can edit (e.g. "setoverwrite_existing: truein consts"). - Snake_case for workflow-body identifiers (input names, step names):
ip_address,check_abuseipdb,format_results. Kebab-case is reserved forinstall.formfield names. - Prefer the dedicated
data.*step types over abusingconsolefor value transformation. Usedata.parseJson,data.set, etc. when you want to compute or restructure data.
The repo ships a small Node script that walks every template, checks its template-metadata block has the required fields, and produces the per-Kibana-version catalogs the CDN serves.
npm install
npm run build:catalogRun it before submitting a PR. A non-zero exit means the catalog publish would fail; the error messages point at the offending file and field.
The script resolves the live main Kibana semver and the supported named minors at run time. Two env vars let you skip those network calls for fast local iteration:
| Env var | Effect |
|---|---|
KIBANA_MAIN_VERSION=9.6.0 |
Skip the fetch of elastic/kibana@main's package.json. |
KIBANA_NAMED_MINORS="" (or "9.5,9.6") |
Skip the GitHub branches API. Empty string = treat as zero named minors. |
GITHUB_TOKEN |
When set, authenticates the branches API call (5,000/h instead of 60/h). CI provides this automatically; locally, export GITHUB_TOKEN=$(gh auth token) works. |
The fastest fully-offline iteration:
KIBANA_MAIN_VERSION=9.6.0 KIBANA_NAMED_MINORS="" npm run build:catalogThe generator enforces only:
- The file parses as YAML.
template-metadatais present with all required fields (slug,version,availability,name,description,categories).- At least one template is discovered.
Deeper authoring invariants — slug ⇄ directory parity, valid version semver and availability range, categories[] membership in the vocab, install.form ⇄ __install__ consistency, and step/connector type validity — are delegated to the separate validation step (planned to run in CI), not the generator.
template-metadata.version is a semver. Bump it on every meaningful content change to a template body:
- Patch (
1.0.0 → 1.0.1) — typo fix, comment tweak, no behaviour change. - Minor (
1.0.0 → 1.1.0) — additive behaviour, new optional install field, additional step that doesn't affect existing callers. - Major (
1.0.0 → 2.0.0) — breaking change to inputs, install form, or the workflow's observable behaviour.
template-metadata.availability is a semver range over Kibana versions. For now, every template carries >=9.5.0. When future Kibana versions retire a step type or connector convention, restrict the range accordingly (">=9.5.0 <9.8.0") and ship a successor template under the same slug with a bumped major.
Multi-version coexistence (the <slug>/<slug>-v2.yaml sibling layout) lands in Kibana 9.6; for the 9.5 starter set, every template is v1.
- Fork the repo and clone your fork.
- Branch from
main:git checkout -b add/<slug>orfix/<slug>-<short-desc>. - Author the template under
library/workflows/<slug>/<slug>.yaml. Follow the rules above. - Run the validator:
npm run build:catalog(with overrides as needed). Resolve any errors. - Commit with a clear message:
Add <slug> templateorFix <slug>: <what changed>. - Open a PR with:
- The slug and one-line description.
- Any migration decisions worth flagging (e.g. "promoted
Xfromconsts:to install form", "swapped rawhttpfor dedicatedvendor.actionstep"). - Validation output (paste the last few lines of
npm run build:catalog). - A short test plan (e.g. "installed in local 9.5, ran with
ip_address=8.8.8.8, output report rendered as expected").
A maintainer will review and either request changes or merge. Once merged, the next push to main republishes the catalog and the template becomes installable from the Kibana UI on every active Kibana version whose semver satisfies your availability: range.
- Be respectful and constructive in reviews.
- Focus feedback on the contribution, not the contributor.
- No real credentials, secrets, or PII committed to YAML — use install-form fields or
consts:placeholders. - Report concerns via GitHub Issues.
Thanks for contributing.