Back to docs

Guide

Authoring a Template Runtime

Use run.command for thin wrappers and script.sh when the template needs real shell logic.

Linkar templates should stay small enough to read in one sitting.

The default decision is:

  • use run.command for a thin one-command wrapper
  • use script.sh when the template needs real shell logic

Start with the smallest useful contract

The smallest useful template is often just linkar_template.yaml plus a local test:

my_template/
  linkar_template.yaml
  test.py

Example:

id: simple_echo
version: 0.1.0
description: Write a greeting file.
params:
  name:
    type: str
    required: true
outputs:
  greeting_file:
    path: greeting_file
run:
  command: >-
    printf 'hello %s\n' "${param:name}" > "${LINKAR_RESULTS_DIR}/greeting_file"

That is cleaner than creating a script.sh whose only job is to forward one command.

How parameters arrive in run.command and script.sh

The preferred authoring style in run.command is the explicit placeholder form.

If the template declares:

params:
  input_fastq:
    type: path
    required: true
  threads:
    type: int
    default: 4

then a command should normally read:

  • ${param:input_fastq}
  • ${param:threads}

Linkar still supports the older implicit shell-variable convention:

  • input_fastq -> ${INPUT_FASTQ}
  • threads -> ${THREADS}

but new templates should prefer ${param:...} because it is clearer to template authors.

Use explicit defaults in the schema whenever possible. That keeps runtime logic small and readable.

When script.sh is the better tool

Use script.sh when the template needs:

  • branching
  • temp files
  • generated config files
  • multiple local commands
  • traps and cleanup

Typical shape:

my_template/
  linkar_template.yaml
  script.sh
  test.sh   or   test.py
  optional support files...

Example:

run:
  entry: script.sh
#!/usr/bin/env bash
set -euo pipefail

if [[ "${PAIRED_END:-true}" == "true" ]]; then
  mytool --r1 "${R1}" --r2 "${R2}" --out "${LINKAR_RESULTS_DIR}"
else
  mytool --r1 "${R1}" --out "${LINKAR_RESULTS_DIR}"
fi

Render command and launcher generation

linkar render ... stages the template bundle and writes one standalone run.sh without executing the template.

The rendered script does not silently cd for you. It expects to be run from inside the rendered directory, so the artifact stays explicit and easy to inspect.

That is especially useful for templates declared as one command:

run:
  command: >-
    pixi run python -m demux_pipeline.cli
    --outdir "${LINKAR_RESULTS_DIR}"
    --bcl_dir "${param:bcl_dir}"
    --samplesheet "${param:samplesheet}"

The rendered directory then contains one launcher, not a template-local wrapper plus a second outer wrapper.

For a run.entry: script.sh template, Linkar keeps the author-owned source name in the template and renders a self-contained run.sh launcher into each run directory. The rendered artifact therefore has one obvious entrypoint without making the source file double as generated output.

Keep testing local and simple

Use one local test entrypoint:

  • test.sh for script-oriented templates
  • test.py for contract inspection, filesystem assertions, and more involved mocking

Normal validation path:

linkar test simple_echo --pack ./examples/packs/basic

Rule of thumb

  • prefer run.command when one command is enough
  • prefer script.sh when logic is real and local
  • switch to run.py when shell stops being clearer