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.

Using modules


Adding a module pulls its content in: its sandbox resources, tabs, pages, activities, and layouts all become part of whatever added it, read-only, owned by the module block that brought them in.

Modules are used the same way in two places. A lab adds modules to build on work someone has already published. A module adds modules for the same reason – a module editor has the same Modules tab, and everything on this page works there identically. Where the two genuinely differ, this page says so.

The one rule worth stating up front: a module’s content is only ever visible one level up. A lab that adds a module does not inherit that module’s own dependencies as its own, and cannot reach into them. See Nesting a module inside a module.

Open the Modules tab in the lab or module editor. The sidebar lists the module blocks by source, with a search box. Select one to see its form.

Add module appears in the sidebar header in edit mode only.

  1. Enter edit mode and open the Modules tab.
  2. Click Add module.
  3. Choose a tab: This team lists your own team’s modules, private and public alike. Community lists public modules from every other team.
  4. Find the module. Use Search modules, and sort by Name or Recently updated.
  5. Click the module’s row.
  6. Choose the version – see Pin a version or give a constraint below.
  7. Click Add module.

Each row shows the module’s slug and its description. On This team, a Private or Public badge; on Community, by <team> instead. On the right, when the module has a published version, when it was published and the version number – otherwise Not published.

A community module with no published version is not listed at all: there would be nothing of it to pull in. Your own team’s are listed regardless, so you can see your whole registry, but a module marked Not published is disabled and cannot be selected until it has a version. Publish a version first.

When you are editing a module, that module is left out of its own picker: nothing can depend on itself.

The new block’s name is derived from the module’s slug, and deduplicated if there is already one: postgres, then postgres-2. You can rename the block afterwards on its form.

Adding a module is an edit-mode change like any other. Review it and publish it the usual way – see Editing and publishing.

The Version field has a Version / Constraint toggle.

Version pins one exact release. Pick it from the list of published versions. It never moves, whatever is published later.

Constraint gives a semver range instead, re-resolved rather than frozen. Resolution takes the highest published version that matches, skipping revoked and deleted ones, and it happens:

  • every time the lab or module is opened in the editor, and
  • every time a session starts, for everything that ends up in the running lab – including modules nested several levels down.

So a ~> 1.4 dependency picks up 1.4.8 the moment it is published, without anything being touched or republished. As you type, the field tells you Resolves to X today, or warns you that the constraint matches no published version.

OperatorExampleMatches
= (or bare)1.4.0Exactly 1.4.0.
>=, >, <=, <>= 1.4.01.4.0 and anything above it.
~>~> 1.2>= 1.2, < 1.3 – the 1.2.x series only. See the note below.
~>~> 1.2.3>= 1.2.3, < 1.3.0.
^^1.2.3>= 1.2.3, < 2.0.0 – anything compatible within the major version.
x wildcard1.2.x>= 1.2.0, < 1.3.0. 1.x is >= 1.0.0, < 2.0.0.
,>= 1.2, < 2.0Both conditions, so anything in 1.x from 1.2 up.

The module block’s form lists every variable the pinned version declares, with the variable’s description as its hint. Set the ones you need; anything left alone uses the module’s own default.

Values are HCL expressions evaluated in the scope that declares the block, so a variable can be set from anything available there – a resource, a variable, a local, or a literal. In a module, that includes the module’s own variables, which is how a value is threaded down through a nesting chain:

# Inside a module, passing its own input down to a nested module
module "postgres" {
source = "acme/postgres"
version = "~> 1.4"
variables = {
database_name = variable.database_name
}
}

If you change the pinned version and the new version no longer declares a variable you had set, the override is stranded: it does nothing, but it is still saved with the block. The form flags stranded overrides in a warning with a Remove button for each.

The module’s content shows up in the tabs of whatever added it, marked with a Module: <team>/<block> badge – just Module: <block> for a local module – that links back to the module block. In sidebar lists the badge is shortened to an icon, with the same text in its tooltip.

  • Sandbox – its sandbox resources
  • Tabs – its tabs
  • Pages – its pages
  • Activities – its tasks, quizzes, and notes
  • Layouts – its layouts
  • Dynamic values – its outputs only

All of it is read-only. You cannot rename, edit, or delete module-owned content from the lab or module that added it – go to the module block and change the version instead, or ask the module’s author for a new release.

Two more things are worth knowing:

  • Only a module’s outputs are listed in the Dynamic values tab. They open in a read-only view. The module’s variables, locals, and secrets are private to it and are not listed. Outputs of modules nested inside a module you added are not listed either, and are not suggested as you type – you can only reach them if the module you added re-exposes them.
  • A module’s layout cannot be the lab’s default layout. The Use as default layout control is not offered on a module-owned layout, which shows Default layout: No instead. This one is lab-only – a module has no default layout of its own.

Outputs, and only outputs:

module.<block>.output.<name>

That is the only kind of module reference the validator accepts. Nothing else inside a module – its resources, its variables, its locals – is reachable from outside it. If you need something a module does not expose, it needs an output block for it.

Fields that take a resource – a terminal’s or service’s target, an editor, a task or exec target, a container’s network – offer the lab’s own resources and, alongside them, any module output that exposes a resource of the right type. Picking one saves it as module.<block>.output.<name>.

An output is only offered when its value is the resource itself, resource.<type>.<name>, or its resource.<type>.<name>.meta.id. An output of some other attribute, such as .meta.name, is still readable in expressions but is not offered in a resource picker. So if consumers should be able to point a terminal at your module’s container, expose the whole resource:

# In the module's outputs.hcl
output "db" {
description = "The database container"
value = resource.container.postgres
}

This is also what makes the one-level-up rule concrete. If your module depends on acme/postgres and your own consumers need its hostname, re-expose it:

# In your module's outputs.hcl
output "database_host" {
description = "Hostname of the bundled database"
value = module.postgres.output.host
}

To change the version, select the module block in the Modules tab and edit its Version field. The source is not editable – a module’s address is its identity, so switching to a different module means removing this block and adding the other one.

To remove a module, select the block, click the delete button in the form header, type the block’s name to confirm, and click Delete. Deletion is unavailable while other resources depend on the block; the button’s tooltip says how many.

The same module can be added more than once, at the same version or at different ones. The instances are fully isolated from each other, because every resource inside a module is namespaced by its block.

module "postgres" {
source = "acme/postgres"
version = "1.4.0"
variables = {
database_name = "orders"
}
}
module "postgres-2" {
source = "acme/postgres"
version = "2.0.1"
variables = {
database_name = "analytics"
}
}

This holds across nesting too. If a lab adds two modules and both depend on acme/postgres at different versions, both versions are resolved and mounted, and neither module can see the other’s copy.

Nesting is limited to 10 levels.

module "postgres" {
source = "acme/postgres"
version = "~> 1.4"
variables = {
database_name = "orders"
max_conns = 50
}
}
module "cache" {
source = "acme/redis"
version = ">= 2.1, < 3.0"
variables = {
max_memory = "256mb"
}
}
resource "container" "app" {
image {
name = "myapp:latest"
}
environment = {
DB_HOST = module.postgres.output.host
DB_PORT = module.postgres.output.port
CACHE_HOST = module.cache.output.host
}
}

A module does not have to come from the registry. A source written as a relative path starting with ./ or ../ names a directory in the same repository:

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

A local module has no version – there is nothing to version, since the content is in the same repository and moves with it. This is the way to split one piece of content into pieces, not a way to share between labs; for that, publish a module to the registry. Local modules are only available to code authors – the UI’s Add module picker adds registry modules.

See the module resource reference for the full source and field rules.