Back to docs

Guide

Build a real pack with templates and bindings

Create a small but realistic pack: one standalone producer template, one consumer template, and one pack-level binding that removes repeated manual wiring.

Use this tutorial when you already understand the basic CLI path and want to build a pack that feels like real reusable work rather than a toy one-template demo.

The goal is to build three things:

  • one standalone template that produces output
  • one standalone template that consumes input
  • one pack-level binding that links the two so the caller does not re-enter the same path again

1. Create the pack layout

Start with a normal pack directory:

message_pack/
  linkar_pack.yaml
  templates/
    produce_message/
    consume_message/

The pack contract lives at the pack root in linkar_pack.yaml.

Validate the complete pack before using or publishing it:

linkar pack validate .

This checks each template contract plus references from bindings to templates, parameters, outputs, and resolver functions.

Each template gets its own directory and should remain runnable and testable on its own.

2. Add a producer template

Create templates/produce_message/linkar_template.yaml:

id: produce_message
version: 0.1.0
description: Write one message into the results directory.
params:
  message:
    type: str
    required: true
outputs:
  results_dir: {}
  message_file:
    path: message.txt
run:
  command: >-
    printf '%s\n' "${param:message}" > "${LINKAR_RESULTS_DIR}/message.txt"

This template is intentionally small:

  • explicit input param
  • explicit declared output
  • run.command because one command is enough

Add a local test at templates/produce_message/test.sh:

#!/usr/bin/env bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "${tmpdir}"' EXIT

linkar run ./templates/produce_message --outdir "${tmpdir}" --param message=hello
test -f "${tmpdir}/results/message.txt"
grep -q '^hello$' "${tmpdir}/results/message.txt"

3. Add a consumer template

Create templates/consume_message/linkar_template.yaml:

id: consume_message
version: 0.1.0
description: Read a previous message file and create a transformed result.
params:
  results_dir:
    type: path
    required: true
outputs:
  results_dir: {}
  transformed_file:
    path: transformed.txt
run:
  entry: script.sh

Then add templates/consume_message/script.sh:

#!/usr/bin/env bash
set -euo pipefail

input_file="${RESULTS_DIR}/message.txt"
tr '[:lower:]' '[:upper:]' < "${input_file}" > "${LINKAR_RESULTS_DIR}/transformed.txt"

This template stays standalone too. It accepts one path and does one job. Nothing in the template definition itself assumes another template exists.

Add a local test at templates/consume_message/test.sh:

#!/usr/bin/env bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "${tmpdir}"' EXIT

mkdir -p "${tmpdir}/source"
printf 'hello\n' > "${tmpdir}/source/message.txt"

linkar run ./templates/consume_message \
  --outdir "${tmpdir}/run" \
  --param results_dir="${tmpdir}/source"

grep -q '^HELLO$' "${tmpdir}/run/results/transformed.txt"

4. Add the pack-level binding

Now connect the templates in linkar_pack.yaml:

templates:
  consume_message:
    params:
      results_dir:
        template: produce_message
        output: results_dir

This is the pack-level chaining rule.

It means:

  • consume_message still declares results_dir as a normal required input
  • when the template is used through this pack binding, Linkar can resolve that input from the latest produce_message run in the project

That keeps the template reusable while still letting the pack remove repeated manual wiring.

5. Test the templates locally

While authoring, run the local tests from each template directory:

cd message_pack/templates/produce_message
bash test.sh
cd message_pack/templates/consume_message
bash test.sh

Then validate through Linkar itself from the pack root or repo root:

linkar test produce_message --pack ./message_pack
linkar test consume_message --pack ./message_pack

That follows the current codebase model:

  • local test.sh or test.py for fast authoring feedback
  • linkar test ... for the real runtime path

6. Use the pack in a project

Create a project and attach the pack with its default binding:

linkar project init --name message-demo
cd message-demo
linkar pack add ../message_pack --id message_pack --binding default

Now run the producer:

linkar run produce_message --message "hello from linkar"

Then run the consumer without manually passing results_dir:

linkar run consume_message

That is the moment when the pack starts paying off. The caller does not have to retype the output path from the previous step, and the reusable connection lives in the pack instead of in shell history.

7. Add custom binding logic only when needed

If output-to-input mapping is not enough, add a binding function:

message_pack/
  linkar_pack.yaml
  functions/
    resolve_message_source.py

Example:

from pathlib import Path


def resolve(ctx):
    latest = ctx.latest_output("results_dir", template_id="produce_message")
    if latest is None:
        raise ValueError("No produce_message run is available")
    return str(Path(latest).resolve())

Then reference it in linkar_pack.yaml:

templates:
  consume_message:
    params:
      results_dir:
        function: resolve_message_source

Use this only when you need real custom resolution behavior. If a simple template plus output rule is enough, keep the binding declarative.

8. Publish and update the pack

Once the pack works locally, publish it to GitHub like any normal Git repository:

git add message_pack
git commit -m "Add message pack"
git push

Users can install the published pack from the Git ref:

linkar config pack add github:ORG/message_pack --id message_pack
linkar templates
linkar run produce_message --message "hello from a remote pack"

When you publish changes, users update their cached checkout explicitly:

linkar config pack update message_pack
linkar config pack list --format yaml

For a project that should record its pack choice, save the pack in project.yaml:

linkar project init --name message-demo
cd message-demo
linkar pack add github:ORG/message_pack --id message_pack --binding default

Git revision is the pack version source of truth. Use Git tags when you want a named release:

linkar pack add github:ORG/message_pack@v2026.05.15 --id message_pack_2026_05_15

See Managing Git-backed packs for the full remote/local pack command guide.

What this tutorial demonstrates

  • templates remain standalone units
  • packs are where reusable chaining logic belongs
  • bindings reduce repeated manual parameter wiring
  • the project records local runs, while the pack carries reusable behavior
  • Git-backed packs are updated explicitly and identified by Git revision

That is the core Linkar model in one small pack.