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 recommendedThe 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: stringImportant: identity fields (
name,version,description) live undermetadata:, not at the top level.exportsandconfigare 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: greetTwo 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.0modules:
- 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.0git tag module1/v1.0.0 && git push origin module1/v1.0.0registries:
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 insidegithub.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: 3Shipping 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:, andenv:load only when the caller usesinstance_profile:orisolate:. 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>inenv:.
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
| Target | How |
|---|---|
| Your own repo | Push 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:; keepexports/configtop-level. - Declare least-privilege
permissions:(onlyshell/http/gityou need). - Give every config property a
defaultanddesc; mark only truly-required onesrequired: 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.lockin consuming projects for reproducible builds.
Anti-patterns
- Don't hardcode environment-specific values — use
config:parameters. - Don't put
name/versionat the top level of the manifest (they go undermetadata:). - Don't break backward compatibility in a MINOR/PATCH release.
- Don't require secrets in
config:— read them from the environment at run time.