Convert existing automation — shell scripts, Python glue, Makefile targets, CI jobs, prompt chains in docs — into checkable .nika.yaml workflows. Use when a script wraps LLM calls or HTTP/file plumbing, a prompt chain lives in a README or notebook, or ad-hoc automation needs audit, cost bounds and replayable traces.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add supernovae-st/nika-plugins --skill nika-migration --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Nika Migration?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/supernovae-st-nika-migration)More formats (shields.io, HTML) on the badges page.
---
name: nika-migration
description: Convert existing automation — shell scripts, Python glue, Makefile targets, CI jobs, prompt chains in docs — into checkable .nika.yaml workflows. Use when a script wraps LLM calls or HTTP/file plumbing, a prompt chain lives in a README or notebook, or ad-hoc automation needs audit, cost bounds and replayable traces.
---
# Migrating existing automation to Nika
A script runs; a workflow is **audited before it runs, bounded while
it runs, and proven after it runs**. Migration is not translation —
it is re-declaring the intent so the checker can see it.
## When to migrate (and when not to)
Migrate when the automation: calls an LLM anywhere · chains
HTTP/file/JSON steps around AI output · is repeated (cron, CI, "run
this every release") · needs a cost bound or an audit trail · is
handed to someone else to run.
Do NOT migrate: one-shot commands · interactive debugging sessions ·
sub-second pure-shell pipelines with zero AI and zero HTTP (a
`Makefile` that only compiles code is already in its best form).
## The mapping table
| In the script | In the workflow |
|---|---|
| a step / function | one task, exactly one verb |
| `curl` / `wget` / `fetch()` helper | `invoke:` `tool: "nika:fetch"` — **for an API, set `mode: raw` or `mode: jq`** (the default `markdown` mode is for pages and escapes JSON bodies) |
| `curl … \| jq` in one breath | ONE fetch task: `mode: jq` + `jq: '<expression>'` — the shape rides the fetch |
| `jq` / `sed` on JSON | `nika:jq` (arg name is `expression`), or an `extract:` binding |
| `cat` / `cp` / `mkdir` / `tee` | `nika:read` / `nika:write` (`create_dirs: true`) |
| in-place file edits | `nika:edit` |
| the LLM call (SDK, `curl` to an API) | `infer:` with `prompt`, `schema?`, `max_tokens` |
| an agent loop (retry-until-good) | `agent:` with `tools` allowlist + `max_turns` |
| a retry/backoff loop around a flaky call | `retry:` on the task — `max_attempts` + `backoff_strategy: exponential` + `jitter: true` (transient provider/network errors only; a wrong prompt never heals by retry) |
| `for item in …` | `for_each:` fan-out |
| `if <condition>` | a `when:` gate |
| `$1` positional parameters | an `inputs:` declaration · supplied with `--var key=value` |
| a value baked into the script | a `const:` entry · read as `${{ const.x }}` |
| `$SOME_SETTING` (non-sensitive) | an `inputs:` declaration with `required: false` and a `default:` · read as `${{ inputs.KEY }}` |
| an env var a CHILD process must see | `permits: { env: [NAME] }` — a child inherits nothing |
| `API_KEY=…` literals | `${{ secrets.X }}` + `secrets:` block with its `egress:` sink |
| step B reads step A's output | `with: { a: "${{ tasks.A.output }}" }` on B — the binding IS the edge — then `${{ with.a }}` in the body |
| step B only waits for step A (no data) | `after: { A: success }` (predicates: `success` · `failure` · `skipped` · `terminal`) |
| the irreversible step (deploy, send, publish) | a confirm gate before it (`nika:prompt`) — human answers at run time |
| what no builtin/MCP covers (git, build tools) | `exec:` with `command:` as ARGV (`["git", "log", "-1"]`) + a row in the exec ledger |
| a pipe, redirect or glob inside the command | `shell:` explicitly — `command:` has no implicit shell |
## The port protocol
1. **Read the source completely.** Inventory: inputs · outputs · side
effects · credentials · the failure the author feared (that guard
clause is the intent — keep it).
2. **Route to a template**: `nika new '?'` lists the embedded
set; pick the OUTER shape (chain · fanout · gate-and-act ·
etl-state · agent-loop · human-gated-ship) and instantiate with
`nika new <template> <file>.nika.yaml`.
3. **Map with the table.** Native-first is the law: `invoke: nika:*`
→ `invoke: mcp:<server>/<tool>` → `exec:` last. Every surviving
`exec:` gets its ledger row (task · command · why no native path ·
unlock that removes it).
4. **Shape under mock**: `model: mock/echo` while the structure
settles — `nika check <file>` after every change, repair from the
diagnostics until exit 0, then `--native-strict`.
5. **Declare the boundary**: `permits:` is mandatory — an effect under
no block refuses `NIKA-AUTH-006` at check.
`nika check <file> --infer-permits` prints the tightest block;
paste it in. The script trusted its author; the workflow trusts
nobody by default (a pure-compute port still declares
`permits: {}`).
6. **Prove parity once**: run the old script and
`nika run <file> --model mock/echo` (or a local model) side by
side on the same input; compare the artifacts. Then pin:
`nika test <file> --update` writes the golden.
7. **Hand off honestly**: the workflow file + the golden + the run
line (`nika run <file> --var … --max-cost-usd <n>`). The human
decides when the old script retires — never delete it yourself.
## Porting a pre-0.106 workflow file
A `.nika.yaml` written before 0.106 can refuse to check today — the
flag day changed what an existing file MEANS. Run `nika check <file>
--fix` first: it migrates three classes mechanically, comment-
preserving and idempotent.
| Dead form | Becomes | Repair |
|---|---|---|
| `vars:` entry, caller-supplied | `inputs:` (typed · `required:` · `default:`) | `--fix` |
| `vars:` entry, fixed value | `const:` | `--fix` |
| `workflow:` envelope key (scalar or object) | `nika: <kebab-case-name>` + `tasks:` map | `--fix` |
| `after: { t: succeeded / failed }` | `success` / `failure` | `--fix` |
| `env:` entry, non-sensitive | `inputs:` (typed · `required: false` · `default:`) | **yours** |
| `env:` entry, a credential | `secrets:` (a store reference) | **yours** |
| `env:` name a child must see | `permits: { env: [NAME] }` | **yours** |
| no `permits:` block, any effect | the inferred block (`--infer-permits`) | **yours** |
`--fix` is atomic-or-nothing per class: on a credential-shaped name, a
typed-only declaration, a flow-style `vars: {…}` header or an empty
block it leaves the file UNTOUCHED and names the reason — it never
guesses. `env:` has NO mechanical repair by design: re-shaping a flat
string map into typed declarations is a classification, and only you
know whether a name is configuration, a credential, or something a
child process needs to see.
## Traps
- Porting a helper script by wrapping it (`exec: node helper.mjs`) is
not a migration — that is `native-first/005`. Unbundle the helper
into fetch/jq/read/write tasks.
- A prompt chain in a doc usually hides implicit state ("then take
the output and…") — make every handoff an explicit
`${{ tasks.X.output }}` reference so the checker can trace it.
- Scripts swallow errors (`|| true`); workflows should not. If the
source ignored a failure, ask whether that was intent or debt —
default to letting the task fail loudly.
- Credentials in the script's environment become DECLARED secrets
with sinks — the engine masks them; the script never did.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!