Build, version, and distribute AI generation workflows as portable OCI artifacts.
Creatifact turns model calls into durable artifacts. It gives agents and CI a stable CLI and JSON interface for generating text, images, and video, then packages the recipe, inputs, outputs, and provenance into content-addressed OCI artifacts. Push them to any OCI-compatible registry. No Docker daemon is required.
flowchart LR
A[Agent or CI] -->|CLI or JSON| C[Creatifact]
C -->|generate or understand| P[Model providers]
P -->|text, images, video| C
C -->|recipe + result + provenance| S[OCI store]
S <--> R[OCI registry]
Creatifact is useful when a generated asset must outlive the model call that created it:
- Durable results — download expiring provider URLs into self-contained packages.
- Traceable provenance — retain effective prompts, model settings, usage, timestamps, and source references.
- Reusable workflows — package a generation recipe once and run it with new inputs anywhere.
- Cost-aware orchestration — execute DAG stages concurrently and reuse stages whose resolved inputs have not changed.
- Provider independence — use one task-oriented contract across built-in and third-party providers.
Creatifact requires Node.js 20 or Node.js 22+.
npm install -g creatifactOr run it without installing:
npx creatifact --versionSet credentials for a built-in provider and choose it as the default:
export ZHIPU_API_KEY="..."
creatifact config set defaults.run.provider zhipuGenerate an image and store it as an OCI package:
creatifact run text2image "a paper crane in the rain" \
--tag demo/crane:v1The command returns one machine-readable JSON document on stdout:
{"ok":true,"kind":"run","data":{"task":"text2image","provider":"zhipu","model":"cogview-4","capability":"image.generate","artifacts":[{"url":"https://..."}],"tag":"demo/crane:v1","outputDir":"...","digest":"sha256:..."}}Inspect the local store:
creatifact package listTo publish the same package, give it a registry-qualified tag and push it:
creatifact tag demo/crane:v1 ghcr.io/acme/crane:v1
creatifact auth login ghcr.io
creatifact push ghcr.io/acme/crane:v1Another machine or agent can retrieve the exact package:
creatifact pull ghcr.io/acme/crane:v1
creatifact package listCreatifact exposes model capabilities as tasks instead of provider-specific API
shapes. run <task> executes a task; run <ref> executes a packaged recipe
(docker-style).
| Task | Input | Output | Common options |
|---|---|---|---|
text2text |
prompt | text | --system, --opt |
image2text |
image and question | text | --input |
video2text |
video and question | text | --input |
text2image |
prompt | image | --opt |
image2image |
image and prompt | image | --image, --opt |
text2video |
prompt | video | --no-wait, --timeout, --interval |
image2video |
image and prompt | video | --image |
frames2video |
first frame, last frame, prompt | video | --first-frame, --last-frame |
embed |
text | vectors | positional inputs or --input |
resume |
saved video job handle | video | --timeout, --interval |
Examples:
# Select a provider explicitly
creatifact run text2image zhipu "a paper crane"
# Select a provider and model
creatifact run image2video kling/kling-3.0-turbo "animate" \
--image first.png
# Let the configured default provider choose a suitable model
creatifact run text2text "explain content-addressed storage"
# Submit an asynchronous video job and resume it later
creatifact run text2video ark "a paper crane taking flight" --no-wait
creatifact run resume '{"providerId":"ark","id":"..."}'Run creatifact models to list providers and creatifact models <provider> to
see verified models and their supported tasks. Model discovery does not require
credentials.
Generation options use --opt key=value. Values are parsed as JSON when
possible, so --opt steps=30 produces a number and --opt watermark=false
produces a boolean. Long prompts can live in a file: --prompt-file <path>
reads and trims the file's content (mutually exclusive with --prompt and the
positional prompt).
A build manifest can contain a run instruction (dockerfile-style RUN). This
packages model defaults, prompts, and input assets into a portable recipe;
credentials are never stored in the package.
Build the recipe without calling the provider, then run it with an override:
creatifact build --bake -t ghcr.io/acme/editorial:v1
creatifact push ghcr.io/acme/editorial:v1
creatifact run ghcr.io/acme/editorial:v1 \
"editorial illustration in red and black"Long prompts can stay in their own files: set run.promptFile to a path
relative to the manifest (mutually exclusive with run.prompt). The file is
read and trimmed at load time — the inlined prompt drives fingerprints and the
packaged recipe, so built artifacts never reference the file again and prompt
edits re-run exactly the stages that consume them.
Without --bake, build executes the run instruction once and packages the
result. Running creatifact run <ref> behaves more like running an image:
it reads the packaged recipe, applies CLI overrides, calls the provider, and
creates a fresh result.
run <ref> accepts a registry reference, a local store tag, or a local OCI
layout. Scalar CLI values replace recipe values, arrays replace arrays, and
--opt merges individual keys.
Use named stages for multi-step workflows. References such as
${cat.tag} and ${cat.digest} create dependency edges automatically;
independent stages run concurrently.
// creatifact.json
{
"stages": [
{
"name": "cat",
"run": { "task": "text2image", "provider": "zhipu", "prompt": "a cat" }
},
{
"name": "dog",
"run": { "task": "text2image", "provider": "zhipu", "prompt": "a dog" }
},
{
"name": "gallery",
"copy": [
{ "from": "${cat.tag}", "paths": ["artifact-1.png"] },
{ "from": "${dog.tag}", "paths": ["artifact-1.png"] }
],
"annotations": {
"org.example.cat.digest": "${cat.digest}",
"org.example.dog.digest": "${dog.digest}"
}
}
]
}creatifact build -t demo/gallery:v1Each stage is a mini build with optional from, copy, assets, run, and
annotations fields. The last stage is also tagged with the build's -t
reference. A stage may reference these outputs from an earlier stage:
tag,digest, andoutputDirtextfor text generation and understanding tasksvectorsanddimensionsfor embeddingsartifacts[N].urlandartifacts[N].base64for media
The default concurrency is 4. Set defaults.build.concurrency to a positive
integer, or to 0 for unlimited concurrency.
Before execution, Creatifact fingerprints each stage's resolved inputs: its generation spec, referenced values, source digests, and asset tree. On the next build, unchanged stages are loaded from the content store without another model call.
# See what would execute or be reused; writes nothing and calls no provider
creatifact build -t demo/gallery:v1 --plan
# Ignore previous fingerprints for this run
creatifact build -t demo/gallery:v1 --forceReuse defaults to "stale". Set defaults.build.reuse to "never" to always
execute. Standalone --output builds have no previous store entry to compare
and therefore run fully.
Incremental reuse is deliberately a policy, not a claim of deterministic model output. Creatifact does not detect silent provider-side model changes, model default changes, or a mutable remote tag moving to a new digest.
Primary execution and configuration commands also have a JSON form. The
command value mirrors the subcommand tree and the remaining fields mirror its
arguments. Supported values include run.*, build, push, pull,
auth.*, config.*, and models.
{
"$schema": "https://github.com/ghraw/unfallenwill/creatifact/main/schemas/creatifact-request.schema.json",
"command": "run.text2image",
"provider": "zhipu",
"prompt": "a paper crane",
"options": { "size": "1024x1024" },
"tag": "demo/crane:v1"
}creatifact -f request.jsonFor run.* requests, trailing CLI flags override file values:
creatifact -f request.json --prompt "a red paper crane" --opt size=2048x2048A request file represents one command. Multi-step orchestration belongs in a
build manifest under stages.
Every non-meta command emits exactly one JSON document:
- Success envelopes go to stdout.
- Progress and warnings go to stderr.
- On failure, the last non-empty stderr line is the error envelope and the process exits non-zero.
--prettyindents JSON; piped output remains plain JSON.--help,--version, and a bare invocation remain human-readable.
{"ok":false,"kind":"run","error":{"code":"E_PROVIDER","message":"...","details":{"category":"quota","status":429}}}| Code | Exit | Meaning |
|---|---|---|
E_INTERNAL |
1 | unclassified internal error |
E_USAGE |
2 | invalid command, arguments, request fields, or local inputs |
E_CONFIG |
3 | invalid or unreadable configuration |
E_AUTH |
4 | missing or invalid credentials |
E_NETWORK |
5 | connection or transport failure |
E_PROVIDER |
6 | provider rejection or failure |
E_IO |
7 | filesystem failure |
E_TIMEOUT |
8 | polling timeout; details include the resumable handle |
Creatifact keeps built, pulled, and generated packages in one shared OCI layout
at ~/.creatifact/store. Blobs are deduplicated by digest and tags are movable
pointers, similar to a local container image store.
creatifact package list
creatifact package serve --browser
creatifact tag demo/crane:v1 demo/crane:latest
creatifact package rm demo/crane:v1Removing a tag deletes blobs only when no other tag references them.
package serve starts a local web UI on 127.0.0.1 (random port, override
with --port) and prints its URL; with --browser it also opens it in the
default browser. The UI is a lazy-loaded waterfall gallery of every package,
and per package the run recipe, result metadata, and every file of its
layers — media rendered inline. Packages can be deleted right from the page
(same semantics as package rm: shared blobs survive). The command prints
the JSON envelope (kind: package.serve, carrying the URL) on stdout and
runs until Ctrl-C. The UI itself is a Svelte app built into the CLI by
npm run build (npm run dev:ui develops it against a running
package serve --port 8765 instance).
Registry commands operate on the shared store by default:
creatifact auth login registry.example.com
creatifact push registry.example.com/team/crane:v1
creatifact pull registry.example.com/team/crane:v1Use --output <dir> to export a standalone OCI layout and --layout <dir> to
push one. Registry credentials resolve in this order:
- A complete CLI username/password pair
- Credentials saved by
creatifact auth login - Anonymous access
Use --password-stdin instead of putting secrets in shell history. Saved
credentials use the Docker-compatible auths shape in the Creatifact config.
--plain-http enables HTTP for a command; loopback registries use HTTP by
default, and auths.<registry>.insecure persists that choice per registry.
Bare references use defaults.registry, which defaults to localhost:5000.
creatifact build reads creatifact.json from the working directory by
default; -f <path> points at any other manifest. A single-package
manifest supports these fields:
| Field | Description |
|---|---|
annotations |
OCI manifest annotations |
from |
One or more registry refs or local OCI layouts whose layers are inherited |
copy |
Selected files or subtrees extracted from source packages into new layers |
assets |
Local directory packed as the top layer, relative to the manifest |
run |
Generation recipe or build-time generation instruction (dockerfile-style RUN) |
{
"$schema": "https://github.com/ghraw/unfallenwill/creatifact/main/schemas/creatifact-build.schema.json",
"annotations": { "org.opencontainers.image.title": "creative-runtime" },
"from": ["registry.example.com/base/assets:v1"],
"copy": [
{ "from": "registry.example.com/team/fonts:v2", "paths": ["fonts"] }
],
"assets": "./project"
}Layer order is from → copy → assets. Relative paths are resolved from the
manifest directory. Copy operations preserve OCI whiteout semantics.
Manifests and -f request files are parsed as JSONC: // and /* */ comments
and trailing commas are accepted, so the examples above work verbatim.
Useful build options:
-t, --tag <ref> Required output tag
-f, --file <path> Manifest path
--dir <path> Override the manifest's assets directory
-o, --output <dir> Export a standalone OCI layout
--annotation k=v Add or override an annotation
--plan Print the dry-run execution plan
--bake Package a recipe without executing it
--force Disable incremental reuse for this run
For editor completion, keep the $schema property shown above or associate the
local schema in VS Code:
{
"json.schemas": [
{
"fileMatch": ["creatifact.json"],
"url": "./schemas/creatifact-build.schema.json"
}
]
}Built-in providers:
| Provider | Credentials |
|---|---|
| Ark | ARK_API_KEY |
| Kling | KLING_API_KEY, or KLING_ACCESS_KEY and KLING_SECRET_KEY |
| MiniMax | MINIMAX_API_KEY |
| Zhipu | ZHIPU_API_KEY or BIGMODEL_API_KEY |
Credentials may also be stored under providers.<id> in the config. Whole
string environment references are resolved at call time, so this keeps the
secret outside the file:
creatifact config set providers.zhipu.apiKey '${ZHIPU_API_KEY}'Use the model catalog before constructing a command:
creatifact models
creatifact models zhipu
creatifact run frames2video zhipu --list-modelsAdd or override provider models under models.<providerId>. This is useful when
a provider releases a model before Creatifact's verified registry is updated.
{
"models": {
"minimax": [
{
"id": "MiniMax-H4",
"mode": "v2",
"capabilities": {
"video.generate": { "textOnly": false, "firstFrame": true }
}
}
]
}
}Unknown model IDs are appended and known IDs are shallowly overridden. Numeric constraints in model notes are hints; the provider API remains authoritative.
Declare a third-party provider module under providers.<id>.module:
{
"providers": {
"my-provider": {
"module": "creatifact-my-provider",
"apiKey": "${MY_PROVIDER_API_KEY}"
}
}
}The module must default-export a factory that returns a Provider. Bare package
names, relative paths, absolute paths, and ~/... paths are supported.
import {
createJsonClient,
defineProvider,
type Provider,
} from "creatifact/providers"
interface Settings {
apiKey?: string
}
export default defineProvider((settings: Settings, env) => {
const apiKey = settings.apiKey ?? env["MY_PROVIDER_API_KEY"]
if (!apiKey) throw new Error("missing MY_PROVIDER_API_KEY")
const client = createJsonClient({
baseUrl: "https://api.example.com",
headers: { authorization: `Bearer ${apiKey}` },
})
const provider: Provider = {
id: "my-provider",
models: [
{
id: "my-image-model",
capabilities: { "image.generate": {} },
lastVerified: "2026-08",
},
],
defaultModels: { "image.generate": "my-image-model" },
imageGenerate: {
async create(req) {
const result = await client.post<{ url: string }>("/v1/images", req)
if (result.isErr()) throw result.error
return { artifacts: [{ url: result.value.url }] }
},
},
}
return provider
})Plugin types and helpers are exported from creatifact/providers. Add
creatifact as a development dependency to compile a plugin; the CLI loads the
module at runtime.
The default config path is ~/.creatifact/config.json. Override its directory
with CREATIFACT_CONFIG_DIR or pass --config-dir <dir> to any command.
{
"defaults": {
"registry": "ghcr.io",
"run": { "provider": "zhipu" },
"build": { "concurrency": 4, "reuse": "stale" }
},
"providers": {
"zhipu": { "apiKey": "${ZHIPU_API_KEY}" }
}
}Manage it through the CLI:
creatifact config path
creatifact config list
creatifact config get defaults.run.provider
creatifact config set defaults.run.provider zhipu
creatifact config resetSecret-looking values are masked by config list and config get. Config
writes are atomic, and a corrupt file fails loudly instead of being ignored.
A generated package contains the effective generation spec and result metadata. Media artifacts are downloaded into a layer when possible so the package does not depend on an expiring CDN URL; the original URL remains in metadata for provenance. If the URL is already unavailable, Creatifact preserves a URL-only record and emits a warning.
Text and embedding tasks return data.text or data.vectors directly. Pass
--tag or --output to package them as text.txt or vectors.json, which can
then be consumed through pkg:// references in another recipe.
Creatifact provides immutable bytes, recorded inputs, and traceable lineage. It does not promise that rerunning a stochastic or silently updated model will produce identical bytes.
npm run dev # Run src/index.ts directly
npm run typecheck # Strict TypeScript check
npm run gen:schemas # Regenerate schemas from src/lib/contract/contract.ts
npm run build # Bundle the CLI
npm test # Run Vitest once
npm run qa # Typecheck, build, test, lint, dependency check, smoke testsrc/lib/contract/contract.ts is the single source of truth for request and
build schemas.
See CONTRIBUTING.md for the full development workflow: the QA gate, commit conventions, architecture rules, and guides for adding providers, commands, and schema changes.