the skeleton register
api-upload-and-create
Your intent sounds like: call a product API: upload a file and create from it One of 10 skeletons the pack ships, green as-is under nika check. Fill the # SLOT: lines, repair from the fix lines, re-check. Machines read the catalog; the binary lists them with nika new '?'.
- 12SLOT pointsthe only lines you edit
- 3patterns lockedarrive correct, stay correct
- 1tool grantedthe minimum for the job
- 3e94a268sha-pinnedre-hashed on every test run
the plan
derived from the file, never drawn by handcreateinvoke
the file, whole
conformance-gated upstream on every spec pushapi-upload-and-create.nika.yaml
# SPDX-License-Identifier: Apache-2.0# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json## TEMPLATE · api-upload-and-create · upload a file and create the resource in one authenticated call.## [create]# nika:fetch · multipart## The product-API job: push the bytes AND the metadata that describe them, get# the created resource back, keep only the fields you need. One round trip —# `multipart:` carries a file part and text parts together, which is what most# create-with-upload endpoints actually want.## Demonstrates ·# - `multipart:` · a file part (`path:`) and text parts (`value:`) in one# request · exactly one of the two per part# - a masked `secrets:` entry in a header, with `egress:` naming the two# places it may travel (the call, and the returned value)# - `mode: jq` response narrowing · the workflow carries fields, not payloads# - `on_error: recover:` · rehearses green with no key and no endpoint, and# the same bindings work once it is wired## WHY ONE CALL, not upload-then-create · a file part is `permits.fs.read`-gated,# and a private read + untrusted ingress + an onward authenticated POST is the# lethal trifecta (NEP-0002): chaining a second call onto the first response# fails NIKA-SEC-009 and demands a human gate. Measured both orderings. One# round trip needs no ceremony — reach for `human-gated-ship` when the job# genuinely wants a person in the loop.## Needs · nothing to rehearse. To run it FOR REAL: a reachable API, the file at# `const.asset_path`, and EXAMPLE_API_KEY in the environment — then delete the# `on_error:` block so a genuine failure is loud.## Run · nika run <file> # offline rehearsal · no key, no endpointnika: v1workflow: id: api-upload-and-create-template # SLOT: kebab-case workflow id # SLOT: one honest sentence — `nika new` matches intents against these words description: "upload + create in one authenticated call"const: api_base: "https://api.example.com" # SLOT: the product API base asset_path: "./out/assets/asset-1.png" # SLOT: the file to uploadsecrets: API_KEY: source: env key: EXAMPLE_API_KEY # SLOT: the OS env var holding the key egress: - to: "nika:fetch" # the send · default-deny otherwise - to: "outputs" # the return value derives from the authed responsepermits: # the blast radius · default-deny once present tools: ["nika:fetch"] # `egress:` above sanctions the FLOW (this secret may ride a fetch); it does # NOT grant the capability to reach anyone. The host is the separate, required # half — an unlisted host is refused at RUN, mid-flight, with the bytes # already on the wire. net: { http: ["api.example.com"] } # SLOT: the host from const.api_base fs: # A `multipart:` file part names a path, and that read crosses the boundary # like any other: measured, without this entry the call dies `NIKA-SEC-004 · # ./out/assets/asset-1.png resolves outside the declared permits.fs.read # boundary`. One exact file, never the tree it sits in. The drift detector # models a multipart part as a read (2026-07-29) — the former NIKA-DRIFT-001 # false hint is closed. read: ["./out/assets/asset-1.png"] # SLOT: keep in step with const.asset_pathtasks: create: invoke: tool: "nika:fetch" args: url: "${{ const.api_base }}/items" # SLOT: the create endpoint method: POST headers: x-api-key: "${{ secrets.API_KEY }}" # SLOT: the auth header name multipart: # Exactly one of `path:` (file) or `value:` (text) per part — a part # carrying both, or neither, is refused before anything is sent. - { name: file, path: "${{ const.asset_path }}" } - { name: title, value: "Rehearsal item" } # SLOT: the metadata fields mode: jq jq: "{ id: .id, url: .url }" # SLOT: the fields downstream needs on_error: # Offline rehearsal · a literal shaped EXACTLY like what the jq above # projects, so `outputs.result` has the same shape on both paths. It also # absorbs the unset key: a `secrets:` entry whose env var is missing fails # NIKA-VAR-001, and a recover catches that too (measured). recover: { id: "rehearsal-0001", url: "https://app.example.com/items/rehearsal-0001" }outputs: result: ${{ tasks.create.output }} # SLOT: the callable contractsha256 3e94a2682deb4c75…. The copy above re-hashes to its pin on every test run (a copy is re-provable, never trusted). Source: api-upload-and-create.nika.yaml in the spec pack · open it in the playground →
the patterns it locks
- fetch `multipart:` (file + text parts)
- masked secrets header
- mode/jq extraction
cross-references
Scaffold it locally: nika new api-upload-and-create my-flow.nika.yaml. Try the shape in the playground, or walk the showcase for the same patterns on real work. Read the spec →