AGPL-3.0-or-later · forever.

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 hand
createinvoke

the file, whole

conformance-gated upstream on every spec push
api-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 contract

sha256 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

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 →