Time: 5 minutes | Difficulty: Beginner | Prerequisites: Python 3.9+, pip
You're going to build a working FLUID plugin — a tiny one that generates a README.md for any fluid contract — and watch it light up in tests. 5 minutes start to finish. No FLUID CLI install needed yet; we'll just verify the plugin works.
By the end you'll know exactly how the SDK works and how to write your own.
A plugin called hello-scaffold that takes any fluid contract and emits one file: README.md. Three pieces of code, ~30 lines total:
hello-scaffold/
├── pyproject.toml # 12 lines — declare the plugin
├── src/hello_scaffold/
│ └── scaffold.py # 18 lines — the plugin itself
└── tests/
└── test_scaffold.py # 4 lines — get 15 conformance tests free
pip install data-product-forge-sdkThat's the only dependency. Zero transitive deps. Import path is fluid_sdk (see the README for the dual-naming rationale).
mkdir hello-scaffold && cd hello-scaffold
mkdir -p src/hello_scaffold tests[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "hello-scaffold"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["data-product-forge-sdk>=0.9,<1"]
[project.entry-points."fluid_build.custom_scaffolds"]
hello = "hello_scaffold.scaffold:HelloScaffold"
[tool.setuptools.packages.find]
where = ["src"]The [project.entry-points."fluid_build.custom_scaffolds"] line is the magic — once installed, the FLUID CLI discovers your plugin automatically.
"""Hello-scaffold — the smallest possible CustomScaffold plugin."""
from fluid_sdk import ContractHelper, CustomScaffold, write_file_action
class HelloScaffold(CustomScaffold):
name = "hello-scaffold"
def plan(self, contract):
c = ContractHelper(contract)
readme = f"# {c.name or c.id or 'Unnamed'}\n\n{c.description or ''}\n"
return [
write_file_action(
path="README.md",
content=readme.encode("utf-8"),
).to_dict(),
]That's the whole plugin. No apply() needed — it's inherited from CustomScaffold and writes the files to disk atomically with path-traversal guards and sha256 verification. You almost never override it.
Also create src/hello_scaffold/__init__.py (empty):
touch src/hello_scaffold/__init__.pyfrom fluid_sdk.testing import CustomScaffoldTestHarness, LOCAL_CONTRACT
from hello_scaffold.scaffold import HelloScaffold
class TestHelloScaffold(CustomScaffoldTestHarness):
plugin_class = HelloScaffold
sample_contracts = [LOCAL_CONTRACT]4 lines of code. ~15 tests for free. Pytest discovers everything inherited from CustomScaffoldTestHarness.
pip install -e .
pip install pytest
pytest -vOutput:
tests/test_scaffold.py::TestHelloScaffold::test_subclasses_base_plugin PASSED
tests/test_scaffold.py::TestHelloScaffold::test_name_is_valid PASSED
tests/test_scaffold.py::TestHelloScaffold::test_role_declared PASSED
tests/test_scaffold.py::TestHelloScaffold::test_constructor_accepts_kwargs PASSED
tests/test_scaffold.py::TestHelloScaffold::test_capabilities_returns_mapping PASSED
tests/test_scaffold.py::TestHelloScaffold::test_plan_returns_list PASSED
tests/test_scaffold.py::TestHelloScaffold::test_plan_actions_have_op PASSED
tests/test_scaffold.py::TestHelloScaffold::test_plan_actions_have_resource_id PASSED
tests/test_scaffold.py::TestHelloScaffold::test_plan_is_deterministic PASSED
tests/test_scaffold.py::TestHelloScaffold::test_get_plugin_info_exists PASSED
tests/test_scaffold.py::TestHelloScaffold::test_get_plugin_info_returns_metadata PASSED
tests/test_scaffold.py::TestHelloScaffold::test_is_custom_scaffold_subclass PASSED
tests/test_scaffold.py::TestHelloScaffold::test_actions_are_write_file_or_skip PASSED
tests/test_scaffold.py::TestHelloScaffold::test_write_file_actions_have_content PASSED
tests/test_scaffold.py::TestHelloScaffold::test_apply_writes_files_to_tempdir PASSED
tests/test_scaffold.py::TestHelloScaffold::test_apply_is_idempotent PASSED
tests/test_scaffold.py::TestHelloScaffold::test_no_path_traversal PASSED
============================== 17 passed in 0.05s ==============================
You wrote 18 lines of plugin code and got 17 conformance tests. Determinism, idempotency, path-traversal safety, role declaration, plan validity — all verified automatically.
Let's run the plugin against a contract by hand (later, the FLUID CLI does this for you):
# demo.py
from pathlib import Path
import tempfile
from hello_scaffold.scaffold import HelloScaffold
contract = {
"fluidVersion": "0.7.4",
"id": "my-first-product",
"name": "My First Product",
"description": "A demo product.",
}
with tempfile.TemporaryDirectory() as tmpdir:
plugin = HelloScaffold(output_root=Path(tmpdir))
actions = plugin.plan(contract)
result = plugin.apply(actions)
print(f"Applied {result.applied} actions, {result.failed} failed")
print(f"Artifacts: {result.artifacts}")
print(f"Wrote:\n{(Path(tmpdir) / 'README.md').read_text()}")python demo.pyOutput:
Applied 1 actions, 0 failed
Artifacts: ['/var/folders/.../README.md']
Wrote:
# My First Product
A demo product.
You just shipped a working FLUID plugin.
┌──────────────────────────────────────────────────────┐
│ Your contract dict │
│ { "id": "my-first-product", "name": "...", ... } │
└──────────────────┬───────────────────────────────────┘
│
▼ plan()
┌──────────────────────────────────────────────────────┐
│ plugin.plan(contract) │
│ 1. Wraps contract in ContractHelper (typed view) │
│ 2. Builds a README string │
│ 3. Returns one write_file action │
└──────────────────┬───────────────────────────────────┘
│
▼ apply() (inherited from CustomScaffold)
┌──────────────────────────────────────────────────────┐
│ plugin.apply(actions) ← inherited reference impl │
│ 1. Decodes content_b64 │
│ 2. Verifies sha256 (plan ↔ apply integrity) │
│ 3. Path-traversal guard │
│ 4. Atomic tempfile + rename to README.md │
│ 5. Returns ExecutionResult │
└──────────────────────────────────────────────────────┘
| If you want to... | Go to |
|---|---|
| Build something realistic — a full CI generator | walkthrough/your-first-real-plugin.md |
| Understand the four plugin roles + pick the right one | reference/role-taxonomy.md |
| See the underlying ABCs + lifecycle in detail | reference/architecture.md |
| Hook your plugin into the FLUID CLI end-to-end | walkthrough/plug-into-fluid-cli.md |
| Browse complete working examples | examples/ |
from fluid_sdk import (
CustomScaffold, # subclass this for file-emitting plugins
InfraProvider, # subclass this for cloud-infra plugins
Validator, # subclass this for contract-inspection plugins
CatalogAdapter, # subclass this for catalog-sync plugins
ContractHelper, # wrap any contract dict — typed read access
write_file_action, # builds a canonical write_file PluginAction
Finding, # validator authors emit these
)
# That's basically everything.Continue → walkthrough/your-first-real-plugin.md