DOC_INDEX
THEME
DOCS/Modules/Creating Modules

Creating Modules

Author, lay out, tag, and publish your own OrchStep modules

This guide covers authoring a module: the manifest format, the two repo layouts, tagging, and publishing.

Module structure

A module is a directory with two required files:

my-module/
├── orchstep-module.yml    # manifest (metadata, permissions, config, exports)
├── orchstep.yml           # the tasks
└── README.md              # optional but recommended

The manifest — orchstep-module.yml

metadata:
  name: greeter                       # [a-z0-9][a-z0-9_-]*
  version: "1.0.0"
  description: "Greet someone by name"
  author: "your-org"
  tags: ["example", "greeting"]

permissions:                          # what the module is allowed to do
  shell: true
  http:
    allow: []                         # allowed host patterns for func: http ([] = none)
  git: false

config:                               # config the consumer may pass (JSON-schema-ish)
  schema:
    properties:
      greeting:
        type: string
        required: false
        default: "Hello"
        desc: "Greeting prefix"

exports:                              # the public tasks consumers can call
  greet:
    desc: "Greet someone by name"
    params:
      - name: name
        type: string
        required: false
        default: "World"
    returns:
      type: object
      properties:
        message:
          type: string

Important: identity fields (name, version, description) live under metadata:, not at the top level. exports and config are top-level.

The workflow — orchstep.yml

A normal OrchStep workflow whose tasks: implement the exported tasks. Use defaults: for config values and produce outputs: for a task's returns:.

name: greeter
desc: "Greeter module"

defaults:
  greeting: "Hello"           # config schema default

tasks:
  greet:
    desc: "Greet someone"
    steps:
      - name: say
        func: shell
        do: "echo '{{ vars.greeting }}, {{ vars.name | default \"World\" }}!'"
        outputs:
          message: "{{ result.output }}"

  main:                       # optional default task
    steps:
      - task: greet

Two repo layouts

How you lay out the repo determines how consumers reference it and how you tag releases.

Single-module repo

The whole repository is one module — put orchstep-module.yml and orchstep.yml at the repo root. Consumers use the full Git URL; versions are plain vX.Y.Z tags.

github.com/your-org/greeter
├── orchstep-module.yml
├── orchstep.yml
└── README.md
tags: v1.0.0, v1.1.0, ...
git tag v1.0.0 && git push origin v1.0.0
modules:
  - name: greeter
    source: "github.com/your-org/greeter"
    version: "^1.0.0"

Monorepo (many modules, one repo)

Host several modules under one scope at modules/@<scope>/<name>/. Each module is versioned independently with a <name>/vX.Y.Z tag. Consumers map the scope to the repo with a registries: block.

github.com/your-org/platform-modules
└── modules/@yourscope/
    ├── module1/   (orchstep-module.yml, orchstep.yml)   tag module1/v1.0.0
    ├── module2/   ...                                   tag module2/v1.0.0
    └── module3/   ...                                   tag module3/v1.0.0
git tag module1/v1.0.0 && git push origin module1/v1.0.0
registries:
  yourscope:
    url: github.com/your-org/platform-modules
modules:
  - name: m1
    source: "@yourscope/module1"
    version: "^1.0.0"

The official tiers (@orchstep/@community/@ai) use exactly this monorepo layout inside github.com/orchstep/orchstep.

Config schema

Declare what consumers may pass under config.schema.properties. Each property supports type, required, default, desc, and (where applicable) enum.

config:
  schema:
    properties:
      replicas:
        type: integer
        required: false
        default: 2
        desc: "Number of replicas"
      log_level:
        type: string
        required: false
        default: "info"
        enum: ["debug", "info", "warn", "error"]
      registry_url:
        type: string
        required: true
        desc: "Docker registry URL"

Consumers pass these on the import:

modules:
  - name: deploy
    source: "@yourscope/deployer"
    version: "^1.0.0"
    config:
      registry_url: "registry.example.com"
      replicas: 3

Shipping a configured instance (environments/, dotenv:, env:)

config.schema is for the library style — values the caller injects. If your module is instead a self-contained unit with its own per-environment config, ship a bundled context and consumers can call it as a configured instance with instance_profile: (see Using Modules).

A bundled module looks like:

modules/billing/
  orchstep.yml          # defaults:, dotenv: [billing.env], env: { ... }
  billing.env           # bundled dotenv (module-relative)
  environments/
    prod.yml            # region: eu, tier: gold
    dev.yml             # region: local, tier: bronze
# modules/billing/orchstep.yml
name: billing
defaults:
  retries: 3
dotenv:
  - billing.env          # resolved relative to the module, not the caller
env:
  BILLING_TIER: bundled
tasks:
  migrate: { steps: [ ... ] }

A consumer then runs it as an instance:

- { module: billing, task: migrate, instance_profile: prod }

Notes for authors:

  • Bundled environments/, dotenv:, and env: load only when the caller uses instance_profile: or isolate:. A plain library call never loads them, so the same module works both ways.
  • Paths in the module's workflow-level dotenv: resolve relative to the module.
  • Keep secrets out of committed bundled files — use an optional, git-ignored dotenv: ["secrets.local.env?"] entry, or declare <required> in env:.

Exports

List each public task under exports: with its params and returns. Keep internal helper tasks out of exports (prefix them with _ to mark them internal).

exports:
  deploy:
    desc: "Deploy to an environment"
    params:
      - { name: env, type: string, required: true }
      - { name: version, type: string, required: true }
    returns:
      type: object
      properties:
        url: { type: string }

Validate before publishing

orchstep module validate ./my-module/

Validation checks structure (manifest fields, exports, files) as errors, and supply-chain concerns (dangerous shell patterns, internal/non-HTTPS URLs, size) as warnings. Add --strict to treat the warnings as errors — that's the same gate orchstep module submit applies for the @ai registry.

Publishing

TargetHow
Your own repoPush the repo + tag (vX.Y.Z single-repo, or <name>/vX.Y.Z monorepo). Consumers reference the URL or your @scope.
@community/Fork github.com/orchstep/orchstep, add modules/@community/<name>/, open a PR.
@ai/orchstep module submit ./my-module/ --agent <name> (auto-validated).
@orchstep/Maintained by the OrchStep team (direct push).

Best practices

  • Put identity under metadata:; keep exports/config top-level.
  • Declare least-privilege permissions: (only shell/http/git you need).
  • Give every config property a default and desc; mark only truly-required ones required: true.
  • Export only your public tasks; prefix internal tasks with _.
  • Tag with semver; bump PATCH for fixes, MINOR for features, MAJOR for breaking changes.
  • Commit orchstep.lock in consuming projects for reproducible builds.

Anti-patterns

  • Don't hardcode environment-specific values — use config: parameters.
  • Don't put name/version at the top level of the manifest (they go under metadata:).
  • Don't break backward compatibility in a MINOR/PATCH release.
  • Don't require secrets in config: — read them from the environment at run time.