Skip to content

Route templates

Route templates are reusable, parameterised route snippets. A template declares a route definition with {{.paramName}} placeholders and a typed parameter list; instantiating it produces a concrete workflow.Definition with the placeholders replaced.

Source: pkg/template/template.go (registry, parameter resolution, deep copy, YAML loader, fluent builder) and pkg/template/builtins.go (eight pre-built templates).

Status of the YAML surface. Templates are first-class in the Go API and ship with a YAML loader (LoadTemplateYAML / LoadTemplateYAMLFile), but they are not yet auto-wired through the top-level workflow YAML — there is no templates: block or from-template: step in pkg/dsl/yaml_loader.go. The intended use today is from Go: define your templates in init(), register them with a template.Registry, and instantiate them into the engine via dsl.NewReifier(...).Reify(def). See Wiring templates in Go below.

Template shape

id: http-to-kafka-bridge
description: "Accept HTTP POST, validate content type, and route to a Kafka topic"
parameters:
  - name: httpPort
    required: true
    type: string
    description: "Port for the HTTP server to listen on"
  - name: kafkaBrokers
    required: true
    type: string
    description: "Comma-separated Kafka broker addresses"
  - name: kafkaTopic
    required: true
    type: string
    description: "Target Kafka topic"
  - name: contentType
    required: false
    default: "application/json"
    type: string
    description: "Expected Content-Type header value"
route:
  trigger:
    uri: "http://0.0.0.0:{{.httpPort}}/ingest"
    params:
      httpMethodRestrict: "POST"
  steps:
    - log:
        message: "Received HTTP request for Kafka bridge"
        level: info
    - validate:
        starlang: |
          def validate(exchange):
              ct = exchange.in_header("Content-Type")
              if ct == None or "{{.contentType}}" not in ct:
                  fail("expected Content-Type {{.contentType}}, got " + str(ct))
    - set-header:
        name: "kafka.topic"
        constant: "{{.kafkaTopic}}"
    - target: "kafka://{{.kafkaBrokers}}/{{.kafkaTopic}}"

Top-level fields

Decoded into template.Template (pkg/template/template.go).

Field Type Default Description
id string required Unique template ID (used to register, look up, and prefix auto-generated instance IDs).
description string empty Free-form. Shown when listing templates.
parameters list empty Typed parameter list — see Parameters.
route object required The parameterised route definition. Use {{.paramName}} placeholders anywhere a string value appears.

Parameters

Each entry maps to template.Parameter.

Field Type Default Description
name string required Used in {{.name}} substitutions.
description string empty Free-form.
required bool false If true, instantiation errors when no value is supplied.
default string empty Used when the param is optional and no value is supplied.
type enum string string, int, bool, duration. Currently advisory — substitution is always string-based; the type is metadata for tooling and documentation.

route: — the parameterised route

Same shape as one entry in the top-level workflows: list (workflow DSL) but without an id: (the instance gets its ID at instantiation time) and using only the step types the template loader supports:

target, log, process, transform, set-header, set-body, remove-header, filter, choice, split, enrich, poll-enrich, wire-tap, multicast, bean, validate, delay, throttle, retry, circuit-breaker, pipeline.

Step types that the loader doesn't recognise return unsupported step type "<key>". If you need them, drive instantiation from Go where you have access to the full workflow.Definition type and every step type.

Placeholder substitution

{{.paramName}} placeholders are replaced by simple string substitution (no Go text/template engine — see pkg/template/template.go: replaceParams). Substitution applies to every string field the template loader knows about:

  • route.description, route.trigger.uri, every value in route.trigger.params.
  • Every step's stringy fields (URIs, Starlang scripts, constants, header names, regex patterns, exception type lists, …).
  • The route ID — set on the instance either explicitly via Instance.WorkflowID or auto-generated as <templateID>-<counter>.
  • The error handler's dead-letter-channel, redelivery-delay, retry-while, and every exception list.

The full walk is in pkg/template/template.go: replaceStep (every step type) and replaceErrorHandler.

Wiring templates in Go

The intended workflow today:

package main

import (
    "github.com/arvenza/helix/pkg/dsl"
    "github.com/arvenza/helix/pkg/engine"
    "github.com/arvenza/helix/pkg/template"
)

func main() {
    // 1. Build a registry and add the built-in templates.
    reg := template.NewRegistry()
    template.RegisterBuiltins(reg)

    // 2. Optionally load custom templates from YAML files.
    if t, err := template.LoadTemplateYAMLFile("templates/my-bridge.yaml"); err == nil {
        _ = reg.Register(t)
    }

    // 3. Instantiate.
    def, err := reg.Instantiate(&template.Instance{
        TemplateID: "http-to-kafka-bridge",
        WorkflowID: "orders-ingest",      // optional; otherwise auto-generated
        Parameters: map[string]string{
            "httpPort":     "8080",
            "kafkaBrokers": "localhost:9092",
            "kafkaTopic":   "orders",
        },
    })
    if err != nil {
        panic(err)
    }

    // 4. Reify into the engine.
    ctx := engine.NewHelixContext()
    rt, err := dsl.NewReifier(ctx).Reify(def)
    if err != nil {
        panic(err)
    }
    _ = rt
}

Built-in templates

template.RegisterBuiltins(reg) adds these to a registry:

ID Description
http-to-kafka-bridge Accept HTTP POST, validate Content-Type, route body to a Kafka topic.
kafka-to-database-sink Consume from a Kafka topic, transform JSON, insert into PostgreSQL.
rest-to-rest-proxy HTTP proxy with optional Starlang transform between two services.
scheduled-database-poll Timer-triggered DB query that publishes results to a target URI.
ai-chat-gateway HTTP /chat endpoint that routes messages to an LLM provider (OpenAI / Anthropic / Ollama).
rag-query-pipeline Embed → vector-search → augment → LLM-generate end-to-end RAG flow.
dead-letter-reprocessor Consume from a DLQ Kafka topic, retry the original target with backoff.
file-to-s3-sync Watch a directory and upload new files to S3.

The full definitions are in pkg/template/builtins.go. Each is a working starting point you can copy + tweak rather than a black box — extract the body with reg.Get("rag-query-pipeline") and inspect the Template.Route field.

Fluent Go builder

The template.Builder mirrors the YAML shape:

tmpl := template.NewTemplate("kafka-to-database-sink").
    Description("Consume from Kafka, transform, insert into PostgreSQL").
    Param("kafkaBrokers", template.Required(), template.Desc("Kafka brokers"), template.TypeString()).
    Param("kafkaTopic",   template.Required(), template.Desc("Source topic")).
    Param("kafkaGroup",   template.Default("helix-db-sink"), template.Desc("Consumer group")).
    Param("pgDSN",        template.Required(), template.Desc("PostgreSQL DSN")).
    Param("pgTable",      template.Required(), template.Desc("Target table")).
    Route(&workflow.Definition{
        Trigger: workflow.TriggerDefinition{
            URI:    "kafka://{{.kafkaBrokers}}/{{.kafkaTopic}}",
            Params: map[string]string{"groupId": "{{.kafkaGroup}}"},
        },
        Steps: []workflow.StepDefinition{
            { Transform: &workflow.TransformDefinition{
                Starlang: `read(payload, "application/json")`,
            }},
            { Target: &workflow.TargetDefinition{
                URI: "pg://{{.pgDSN}}/{{.pgTable}}",
            }},
        },
    }).
    Build()

Parameter options: Required(), Default("x"), Desc("..."), TypeString() / TypeInt() / TypeBool() / TypeDuration().

Behaviour notes / gotchas

  • Templates are not currently loaded from the top-level YAML document. pkg/dsl/yaml_loader.go has no templates: field. To use a template today, instantiate it from Go (see above) or hand-render it (run Instantiate, marshal the result to YAML, paste into workflows:).
  • Required parameters with empty-string values still pass. The loader only checks "was the key supplied?" — pass "" for an optional default if you really mean empty.
  • String substitution is naive. {{.x}} matches anywhere it appears, so don't pick parameter names that collide with literal strings in your route. Prefer descriptive names like kafkaBrokers, not host.
  • Step coverage in the YAML loader is partial. Only the 21 step types listed under route: are recognised by LoadTemplateYAML. Use the Go builder if you need a step type not on that list (do-try, aggregate, agent, every rag-*, every inline http-request: / db-query: / …).
  • Deep copy is exhaustive. Instantiating the same template twice produces two independent workflow.Definitions — placeholder replacement never mutates the template.

See also

Source

  • pkg/template/template.go — registry, parameter resolution, deep copy, YAML loader, fluent builder
  • pkg/template/builtins.go — the 8 pre-built templates
  • pkg/workflow/definition.go — the Definition shape templates produce