Developing workflow steps
Custom step types are installed with specify workflow step. A step is a
directory package containing metadata and executable Python.
Custom step packages
my-step/
├── step.yml # required, at the package root
├── __init__.py # required, at the package root
└── helpers.py # optional nested modules and data files
step.yml declares the step's identity. step.type_key must exactly match the
<step_id> passed on the command line — the ID is never inferred from package
content:
step:
type_key: my-step
name: My Step
version: 0.1.0
author: you
description: What this step does
__init__.py must define a StepBase subclass whose type_key matches:
from specify_cli.workflows.base import StepBase, StepResult
class MyStep(StepBase):
type_key = "my-step"
def execute(self, config, context):
return StepResult(output={"ok": True})
Package validation
Every source is validated identically before anything is committed:
step.ymland__init__.pymust be regular, non-symlink files at the package root.- The package tree is copied recursively (relative imports, nested helper modules, and data files are supported). A symlinked package root, any descendant symlink, and any filesystem object that is not a regular file or directory are rejected.
.git,__pycache__, and.DS_Storeentries are skipped without being inspected: they are not copied, excluded directories are not entered, and they do not count toward any limit.- The installed-package policy permits at most 512 retained entries (files and directories combined), at most 32 levels of directory nesting, and 50 MiB of retained content.
- Archive URLs also pass transport/extraction safety limits before package validation: at most 512 archive entries, 50 MiB downloaded or extracted, and 10 MiB per archive member. Catalog files have a 50 MiB per-response bound.
- Installation validates and copies the package but does not import or
execute
__init__.py. Installed custom step modules are loaded during startup ofworkflow add,workflow run, andworkflow resume, before any particular custom step necessarily executes.
Security note: Loading a custom step runs its Python with your privileges. Only install and retain step packages from sources you trust.
Implement a custom step
Subclass StepBase, give the package and implementation the same type_key, and return explicit status, outputs, and errors. Override validate() and guard invalid configuration in execute() too: execution does not automatically call the validator.
The registry shares a single instance of each type. Concurrent fan-out can call it from several threads, so keep execution stateless and thread-safe; store per-run data in config and context, not self. Let the engine own checkpoints and lifecycle transitions.
Use specify workflow step add my-step --dev ./my-step for local iteration and --force after editing. See the step installation reference for source validation and replacement failure behavior.
Test locally
Test valid and invalid configuration, returned statuses, outputs, and errors. Exercise the type through a small workflow, including relevant resume, nested-step, and concurrent execution paths. Use a disposable initialized project when the package or workflow can produce side effects.
Bundle-local limitation
A bundle's provides.steps still resolves only through the active step
catalogs. Bundle-local steps/<id>/ payloads and relative
provides.steps[].source overrides are not resolved in this release, so
such steps are not installable offline. See the bundle offline limits.