| name | create-workflow-block |
| description | Author a new Roboflow Workflows block under `inference/core/workflows/core_steps/**`. Trigger on a new/changed `vN.py` defining a `WorkflowBlockManifest` + `WorkflowBlock` pair; new `load_blocks()`/`KINDS_SERIALIZERS` entries in `core_steps/loader.py`; new `Kind(...)` in `execution_engine/entities/types.py`; or a request to "create/add a workflow block" or "expose X as a workflow step" (model / transform / sink / visualization / flow control). |
Creating a Workflow block
The playbook for authoring a new Roboflow Workflows block — a
roboflow_core/{family}@v{N} step under inference/core/workflows/core_steps/.
How to use this skill. Read What a Workflow is and Anatomy of a block, run the
New-block checklist, then open the one Block category reference under references/
that matches what you're building for its mental model and canonical example blocks.
Always match an existing sibling block before writing a line. Authoritative depth lives in
docs/workflows/**; this skill is a map into it. Reviewer-side counterpart:
review-workflows-blocks.
What a Workflow is (mental model)
A Workflow is a JSON definition (inputs + steps + outputs) written in the Workflows language. It is not run directly — a Compiler parses it against the pool of installed blocks, and an Execution Engine (EE) runs the resulting DAG. As a block author you never touch the EE internals; you write a Python class the EE instantiates and calls.
- Definition → Compiler → Execution Engine. Each definition declares an EE version (
version: 1.x means >=1.x,<2.0.0). A step is one instance of a block ({"type": "...@v1", "name": "my_step", ...}); step inputs are either literals or selectors ($inputs.<name>, $steps.<step>.<output>) resolved at runtime. Concepts: docs/workflows/understanding.md, docs/workflows/workflow_execution.md, docs/workflows/definitions.md.
- Data flows as typed
kinds. A step output has a kind (e.g. image, object_detection_prediction); a downstream input declaring the same kind is assumed compatible — compile-time verification without runtime checks. Kinds are defined as Kind(...) constants in inference/core/workflows/execution_engine/entities/types.py (the per-kind docs under docs/workflows/kinds/ are build-time generated).
- Batch orientation & dimensionality. The EE is batch-oriented: it fans data through steps as batches, and each datapoint sits at a dimensionality level. A block can keep, increase (crop-per-detection), or decrease that level — the single most important concept for non-trivial blocks. See
docs/workflows/workflow_execution.md and docs/workflows/create_workflow_block.md.
- Compiler/EE roles:
docs/workflows/workflows_compiler.md, docs/workflows/workflows_execution_engine.md.
Anatomy of a block
A block is two classes in a vN.py module: a WorkflowBlockManifest (the schema/prototype for a step declaration) and a WorkflowBlock (the logic). Primary guide: docs/workflows/create_workflow_block.md. Confirmed real shape: inference/core/workflows/core_steps/transformations/detection_offset/v1.py.
Manifest — a pydantic model subclassing WorkflowBlockManifest (from inference.core.workflows.prototypes.block):
type: Literal["roboflow_core/<family>@v1", "<Alias>"] — the discriminator the Compiler uses to pick this manifest when parsing a step. The Literal may carry a legacy alias as a second value (see the type field in detection_offset/v1.py, ["roboflow_core/detection_offset@v1", "DetectionOffset"]).
- Inputs are ordinary fields. Use
Selector(kind=[...]) for data references and Union[<literal type>, Selector(kind=[...])] when a value may be hardcoded or selected (e.g. Union[PositiveInt, Selector(kind=[INTEGER_KIND])]). Wrap each with pydantic Field(description=..., examples=...) — the user-facing docs are generated from these descriptions/examples (see Docs are autogenerated below). model_config = ConfigDict(json_schema_extra={...}) carries UI metadata (name, block_type, icon).
@classmethod describe_outputs() -> List[OutputDefinition] — one OutputDefinition(name=..., kind=[...]) per output the run() dict must supply. For manifest-dependent outputs, return a single name="*", kind=[WILDCARD_KIND] and add instance method get_actual_outputs().
@classmethod get_execution_engine_compatibility() -> Optional[str] — semver range, e.g. ">=1.3.0,<2.0.0"; gates loading.
- Batch/dimensionality hooks (all classmethods, opt-in):
get_parameters_accepting_batches() → names of params delivered as Batch[...]; get_parameters_accepting_batches_and_scalars() → mixed params; get_output_dimensionality_offset() → +1/-1 when the block changes nesting level.
Block — subclass WorkflowBlock:
@classmethod get_manifest() -> Type[WorkflowBlockManifest] returns the manifest class.
def run(self, ...) -> BlockResult — the EE calls this with kwargs matching manifest input names. Image inputs arrive as WorkflowImageData (use .numpy_image); scalar params arrive as plain values; batch params (if declared) arrive as Batch[...]. Return a dict {output_name: value} (or, for batch/dimensionality-increasing blocks, a list of such dicts — a None entry drops that datapoint downstream). __init__ may hold reusable state (persists across run() calls, e.g. per-frame video).
- Imports:
WorkflowBlock, WorkflowBlockManifest, BlockResult from inference.core.workflows.prototypes.block; Batch, OutputDefinition, WorkflowImageData from inference.core.workflows.execution_engine.entities.base.
Universal invariants (every category)
These hold regardless of category — the per-category references only add nuance on top.
- Output keys ==
describe_outputs(). Every key your run() returns must be named in describe_outputs(), on every return path (empty / error / early-exit branches included), and the names must stay stable across versions — downstream steps bind to them.
- A block is invisible until registered. For
roboflow_core, add the import + load_blocks() list entry in inference/core/workflows/core_steps/loader.py (see DetectionOffsetBlockV1). External plugins expose it via load_blocks() in the plugin __init__.py (WORKFLOWS_PLUGINS="plugin_a,plugin_b"). Forgetting this means the block does not exist to the EE.
- New kind ⇒ serializer. A kind not already round-trippable needs a serializer/deserializer pair registered in
KINDS_SERIALIZERS/KINDS_DESERIALIZERS in loader.py (e.g. deserialize_rgb_color_kind for RGB_COLOR_KIND), or a new key written into sv.Detections.data handled in core_steps/common/serializers.py. Unregistered kinds fall through to serialize_wildcard_kind and may be dropped on the REMOTE / API boundary. Reuse existing detection/image kinds unless you truly need a new one.
- Parent-coordinate attach is load-bearing. Any block that produces or moves
sv.Detections must attach parent-coordinate metadata (attach_parents_coordinates_to_batch_of_sv_detections / attach_parents_coordinates_to_sv_detections in core_steps/common/utils.py, or WorkflowImageData.create_crop(...)). Omitting it silently breaks re-projecting boxes onto the original frame for cropped/tiled inputs.
- Stateful ⇒ restrictions + LOCAL-only where required. A block holding cross-frame state must key it by
image.video_metadata.video_identifier, evict it (bounded cache), and declare get_restrictions() returning the relevant RuntimeRestriction constants from inference/core/workflows/prototypes/block.py — STATEFUL_VIDEO_HTTP_SOFT_RESTRICTION, STILL_IMAGE_INPUT_SOFT_RESTRICTION, COOLDOWN_HTTP_SOFT_RESTRICTION (SOFT), or a Severity.HARD restriction on StepExecutionMode.REMOTE when remote execution is incoherent (then also raise NotImplementedError in run()). State silently no-ops behind stateless/multi-replica HTTP.
- LOCAL vs REMOTE parity. Model blocks with a
run_locally/run_remotely split must mirror every knob into InferenceConfiguration on the REMOTE path (note renames like confidence → confidence_threshold). A knob wired to only one path is a correctness bug, not a style nit.
- Docs are autogenerated — do NOT hand-write them.
docs/workflows/blocks/<block>.md (and docs/workflows/kinds/index.md) are generated by development/docs/build_block_docs.py (write_individual_block_pages / write_kinds_docs). A hand-written page gets overwritten. Instead, put rich content into Field(description=..., examples=...) and the manifest long-description — the generator renders the page from those.
New-block checklist
Reviewer's checklist for block PRs: review-workflows-blocks.
Kinds & selectors (the type system)
Kinds are the Workflows type system. Each kind pairs a semantic name (image, point), a Python representation blocks receive (e.g. object_detection_prediction → sv.Detections), and an optional serialized representation for the wire. No polymorphism — express alternatives as a union, i.e. Selector(kind=[A_KIND, B_KIND]) (see the multi-kind predictions input in detection_offset/v1.py).
- Where kinds live:
Kind(...) constants in inference/core/workflows/execution_engine/entities/types.py (e.g. IMAGE_KIND, OBJECT_DETECTION_PREDICTION_KIND, FLOAT_ZERO_TO_ONE_KIND, WILDCARD_KIND). Per-kind docs pages under docs/workflows/kinds/ are build-time generated from these.
- Selector vs literal.
Selector(kind=[...]) accepts only runtime references ($inputs.* / $steps.*.*); a bare Python type accepts only hardcoded values; Union[type, Selector(kind=[...])] accepts both. StepSelector ($steps.<step>, no output) marks a flow-control block.
- Batch vs non-batch kinds. A kind name is orthogonal to batching — whether a param arrives as a scalar or
Batch[...] is decided by get_parameters_accepting_batches(), not the kind. (Historically Batch[X] vs X were separate kinds; unified in inference 0.18.0.) Deeper representation notes: docs/workflows/internal_data_types.md.
Versioning & bundling
Rules from docs/workflows/versioning.md and docs/workflows/blocks_bundling.md:
- Bug-fix in place; anything else is a new version. Only patch the existing
vN.py for bug fixes. Behavioral/interface changes create v(N+1).py in a new module under the block package — stability over DRY; code duplication is accepted and blocks stay independent.
- Type identifiers & aliases. Convention
{plugin}/{block_family}@v{X} (e.g. roboflow_core/detection_offset@v1). The type Literal may list a legacy alias ("DetectionOffset") so old definitions keep parsing.
- EE compatibility. Every manifest returns a semver range from
get_execution_engine_compatibility(); if a block needs a feature added in 1.3.7, declare ">=1.3.7,<2.0.0" — derive the floor from the changelog, never copy-paste the default. A feature still under ## Unreleased has no version to declare against: the EE version must be placed and bumped (maintainer-coordinated) before the block ships. A definition's version: 1.1.0 resolves to >=1.1.0,<2.0.0. History: docs/workflows/execution_engine_changelog.md.
- Plugin layout & the
__init__.py requirement. A plugin is a Python package: {plugin}/{block_name}/v1.py per block, plus a main __init__.py exposing load_blocks() (required), optionally load_kinds(), REGISTERED_INITIALIZERS, and KINDS_SERIALIZERS/KINDS_DESERIALIZERS. See docs/workflows/blocks_bundling.md.
Block categories (per-category references)
Blocks live under inference/core/workflows/core_steps/<category>/. The 16 dirs (~210
versioned blocks) group into 8 category maps — open the one matching what you're building:
(The models dir is ~75 blocks total, split across the two model references.)
Going deep (docs assets)
Curated index — read the doc for the matching need:
docs/workflows/create_workflow_block.md — PRIMARY. Full walkthrough: manifest → block → registration, advanced batch inputs, flow-control blocks, nested/named selectors, input/output dimensionality vs run() signature.
docs/workflows/custom_python_code_blocks.md — in-definition Python block instead of a packaged plugin block.
docs/workflows/inner_workflow_design.md — when your block runs a nested/sub-workflow.
docs/workflows/testing.md — unit + integration tests (also create_workflow_block.md integration-test template).
docs/workflows/blocks_connections.md — how kinds drive which steps can connect.
docs/workflows/workflows_execution_engine.md + docs/workflows/workflow_execution.md — EE internals, batch fan-out, dimensionality, empty/conditional datapoints.
docs/workflows/internal_data_types.md — WorkflowImageData, Batch, and the concrete Python types behind each kind.
docs/workflows/blocks_bundling.md + docs/workflows/versioning.md — plugin packaging, loaders, (de)serializers, version lifecycle.
docs/workflows/batch_processing/ and docs/workflows/video_processing/ — batch-heavy and video/stateful block patterns.
docs/workflows/execution_engine_changelog.md — which EE version introduced a feature (pin get_execution_engine_compatibility() accordingly).