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.commandbecause 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_messagestill declaresresults_diras a normal required input- when the template is used through this pack binding, Linkar can resolve that input from the
latest
produce_messagerun 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.shortest.pyfor 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.