Skip to content

You are viewing documentation for Instruqt 2.0 Labs which is in Beta currently. Official release date - 29 September, 2026. For Tracks documentation, please visit docs.instruqt.com.

Lab Structure


A lab is a directory: a set of HCL configuration files at its root, plus the content those files reference — Markdown instructions, scripts, assets, and other supporting files in subdirectories. The lab directory can be the root of a git repository or a subdirectory inside a shared one.

Three rules govern the structure:

  1. File names are conventions, not requirements. The platform loads every .hcl file at the top level of the lab directory, whatever it is called. You are free to keep everything in one file or split resources across many.
  2. Discovery is flat. Only top-level .hcl files are loaded. Configuration in a subdirectory is parsed only when a module block references it.
  3. Exactly one lab resource must exist across all files. The file that declares it is the lab’s entrypoint — platform-managed metadata, such as the instruqt config-version block, is written there.

When the CLI or the platform loads a lab, it reads the top level of the lab directory and parses every file with the .hcl extension. Directories are skipped, so an .hcl file inside a subdirectory is ignored — unless a module block points at it:

module "chapter" {
source = "./modules/chapter"
}

Local module sources must start with ./ or ../ and must stay inside the lab directory. Within a module directory the same rule applies again: its top-level .hcl files are loaded, and deeper nesting requires further module blocks (up to 10 levels).

File paths in configuration — a page’s file, a note’s file, task and exec scripts — are resolved relative to the lab root, and validation checks they exist. Functions that read files, like file(), refuse paths that leave the lab directory (no ../ escapes).

None of these file names is required — they are the conventions used by the CLI skeleton, the tutorials, and the visual editor. When you add a resource in the UI, it is written to the conventional file for that resource type, creating the file if needed.

FileTypical contents
main.hclThe lab resource: metadata, settings, and the content block that arranges chapters and pages. The entrypoint of the lab.
layouts.hcllayout resources defining the panel arrangement.
sandboxes.hclInfrastructure: networks, containers, VMs, clusters, cloud accounts, and supporting resources such as exec, copy, and template.
tabs.hclWhat learners see: terminal, service, editor, note, external_website, virtual_browser, and cloud_credentials resources.
pages.hclpage resources pointing at instruction Markdown files. The UI writes pages to content.hcl instead.
tasks.hcltask resources with their check/solve conditions.
quizzes.hclquiz resources and their question resources.
secrets.hclsecret resources referencing team-managed secrets.
certificates.hclcertificate_ca and certificate_leaf resources.
variables.hcl, locals.hcl, outputs.hcl, modules.hclWhere the UI places variable, local, output, and module definitions.

The CLI skeleton (instruqt lab init, template skeleton) starts with only main.hcl and layouts.hcl — the minimum needed to launch a lab — plus placeholder READMEs in the content directories. It defines no network and no sandbox resources; the infrastructure tutorial adds those in sandboxes.hcl.

A fully grown lab, using every convention from these docs, looks like this:

  • Directorymy-lab/
    • main.hcl the lab resource, settings, chapters and pages
    • layouts.hcl layout resources
    • pages.hcl page resources
    • tasks.hcl task resources
    • quizzes.hcl quiz and question resources
    • sandboxes.hcl networks, containers, VMs, clusters
    • tabs.hcl terminals, services, editors, notes
    • secrets.hcl team secret references
    • certificates.hcl certificate resources
    • variables.hcl variable definitions
    • outputs.hcl output values
    • Directoryinstructions/ page content, one Markdown file per page
      • Directorygetting-started/
        • welcome.md
        • first-steps.md
    • Directorynotes/ note tab content
      • cheat-sheet.md
    • Directoryscripts/ task and exec scripts
      • Directorytask/
        • Directoryedit_homepage/
          • check.sh
          • solve.sh
      • Directoryexec/
        • Directorysetup/
          • script.sh
    • Directoryassets/ media served at /assets inside the sandbox
      • diagram.png
    • Directoryfiles/ arbitrary files used by sandbox resources
      • app.conf
    • Directorytemplates/ template resource sources
      • nginx.conf.tpl
    • Directorymodules/ reusable configuration, loaded via module blocks
      • Directoryweb-server/
        • main.hcl
        • variables.hcl
        • outputs.hcl
    • README.md

Only a few of these names carry actual behavior:

  • assets/ is the one enforced directory name. The lab’s icon must live under assets/ (or be an external URL), and the directory is served from the sandbox at /assets, which is why instructions reference images as /assets/diagram.png.
  • Page and note files must be Markdown (*.md) and must exist — validation fails otherwise. Task and exec script files must exist too; their names are free since configuration references them by path.
  • Everything else — instructions/ organized by chapter, scripts/task/<name>/ and scripts/exec/<name>/script.sh, files/, templates/, notes/ — is the convention the platform itself follows when it generates files for you, and the layout instruqt lab init scaffolds.

Resources that consume directories of input files use free-form names as well: Kubernetes manifests (k8s/), Nomad jobs (jobs/) and agent configs (nomad/, consul/), Helm charts (charts/), Terraform sources (terraform/), IAM policies (policies/), and configuration inputs (config/) are all just paths you pass to the corresponding resource.

Everything hangs off the lab resource: it arranges the content through its content block and picks the presentation through its layout reference. This is the wiring in its shortest form:

resource "lab" "main" {
title = "My First Lab"
description = "A lab showing how the pieces wire together."
layout = resource.layout.two_column # default layout for every page
content {
chapter "getting_started" { # chapters are blocks, not resources
title = "Getting Started"
page "welcome" {
reference = resource.page.welcome # each page slot points at a page resource
}
}
}
}
resource "page" "welcome" {
title = "Welcome"
file = "instructions/getting_started/welcome.md"
activities = {
edit_homepage = resource.task.edit_homepage # ids used in the page's markdown
}
}
resource "terminal" "shell" {
target = resource.container.workstation # tabs attach to sandbox resources
}

Four things to know about this graph:

  • Chapters and pages are blocks inside the lab resource, not standalone resources. The only standalone content resource is page, and every page slot in the content block must reference one. Chapters and pages can override the lab’s default layout.
  • Pages carry the interactivity. A page’s file is its Markdown content, and its activities map connects ids to task and quiz resources. The pairing is validated in both directions: every id in the map must appear as an <instruqt-task id="..."> or <instruqt-quiz id="..."> tag in that page’s Markdown. A quiz references its questions; tasks and quizzes can gate on prerequisites (chapters, tasks, or quizzes).
  • Layouts decide what is on screen. A layout’s instructions panel renders the current page, and every other tab has a target: a note (its file is Markdown, by convention in notes/), an external_website or virtual_browser (a URL), or a sandbox-facing resource.
  • Sandbox-facing targets are type-checked. A terminal or editor attaches to a container or vm; a service also to a k8s_cluster, nomad_cluster, or ingress; cloud_credentials to cloud accounts. A task runs its condition scripts on a container or vm; exec targets a container.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1f2440', 'primaryTextColor':'#e2e8f0', 'primaryBorderColor':'#2a2f45', 'lineColor':'#ffffff', 'secondaryColor':'#141829', 'tertiaryColor':'#181c32', 'background':'#181c32', 'mainBkg':'#181c32', 'secondBkg':'#181c32', 'tertiaryTextColor':'#e2e8f0', 'edgeLabelBackground':'#181c32', 'clusterBkg':'#181c32'}}}%%
graph TD
  LAB["lab"] -->|layout| LAYOUT["layout"]
  LAB -->|"content: chapter, page"| PAGE["page"]
  PAGE -->|activities| ACT["task / quiz"]
  LAYOUT -->|"tab target"| TAB["terminal / service / editor"]
  ACT -->|"task target: container / vm"| SANDBOX["sandbox resources"]
  TAB -->|target| SANDBOX

  style LAB fill:#667eea,stroke:#2a2f45,color:#ffffff
  style PAGE fill:#667eea,stroke:#2a2f45,color:#ffffff
  style LAYOUT fill:#667eea,stroke:#2a2f45,color:#ffffff
  style ACT fill:#667eea,stroke:#2a2f45,color:#ffffff
  style TAB fill:#667eea,stroke:#2a2f45,color:#ffffff
  style SANDBOX fill:#43e97b,stroke:#2a2f45,color:#1a1a2e

The diagram shows references only — the files each resource reads (instructions/, notes/, scripts/) are covered in the directory structure above. Layout tabs can also point at a note, external_website, virtual_browser, or cloud_credentials resource; those render content rather than attaching to a sandbox machine.

References resolve within their scope: resources inside a module see only that module’s resources, and the parent configuration reads module results through outputs, as module.NAME.output.OUTPUT.