---
name: unship
description: "Compare AI agent-made UI variants locally in a real app, then keep one and clean up unused temporary code."
category: development
risk: critical
source: community
source_repo: mbenhard/unship
source_type: community
date_added: "2026-06-07"
author: Marcus Benhard
tags: [ui-variants, frontend, local-first, coding-agents]
tools: [claude-code, antigravity, cursor, gemini-cli, codex-cli, opencode]
license: "MIT"
license_source: "https://github.com/mbenhard/unship/blob/main/LICENSE"
plugin:
  targets:
    codex: blocked
    claude: blocked
---

# Unship

## Overview

Unship is a local workflow for comparing AI-generated UI alternatives in the real application instead of accepting one generated version at a time. It adds temporary source-level variants, shows a local browser picker, and then cleans up the unused options after the user chooses.

This skill is for frontend iteration with coding agents. It is not production A/B testing, analytics, feature flagging, or a hosted experiment service.

## When to Use This Skill

- Use when the user wants to compare multiple UI, layout, copy, state, flow, or design-system alternatives.
- Use when a coding agent should create several temporary options in real source code and let the user judge them in the running local app.
- Use when the user chooses a visible option and wants the losing temporary code removed before shipping.

## Do Not Use This Skill When

- The user needs production experiments, traffic splitting, analytics, or feature flags.
- The app cannot safely render inactive hidden variants because of duplicate active IDs, global scripts, analytics triggers, focus traps, destructive actions, or autoplay side effects.
- The user has not authorized local source edits.

## How It Works

### 1. Install or reuse Unship

Prefer the project-local binary when it exists:

```bash
./node_modules/.bin/unship doctor --json --no-update-check
```

Otherwise install a reviewed, exact CLI version into the project and then run the local binary:

```bash
npm install --save-dev @unship/cli@<reviewed-version>
./node_modules/.bin/unship doctor --json --no-update-check
```

If setup is needed for the local picker, run:

```bash
./node_modules/.bin/unship setup --json
```

Patch only the smallest development-only mount point required to load the picker in the local preview.

### 2. Create temporary variants

Inspect the relevant page, component, route, or rendered artifact. Add the smallest source-level comparison that lets the user judge real options in context.

Use Unship markup:

```html
<section data-unship-pick="Hero">
  <div data-unship-option="Current">...</div>
  <div data-unship-option="Proof-led" hidden>...</div>
  <div data-unship-option="Visual" hidden>...</div>
</section>
```

Keep option labels short and visible. Prefer 2-4 meaningful alternatives unless the user asked for a specific count.

### 3. Verify comparison readiness

Before handing off to the user, check that:

- the expected `data-unship-pick` group exists;
- the expected option labels exist;
- options are direct children of the group;
- exactly one option is initially visible;
- hidden inactive options remain hidden.

### 4. Let the user choose

Tell the user the group label, option labels, setup status, and any detected local preview server hints. The user chooses by naming a visible option label in chat.

### 5. Clean up after selection

When the user picks a winner, keep that option's real source and remove losing options for that group. Remove temporary `data-unship-*` attributes from settled source.

For final cleanup before shipping, remove all Unship artifacts and run:

```bash
./node_modules/.bin/unship check --json
```

Do not claim cleanup is complete until the check reports clean.

## Best Practices

- Keep Unship work local and temporary.
- Preserve the existing app design language unless the user explicitly asks for a different direction.
- Avoid unrelated refactors while variants are temporary.
- Do not put custom tabs, app preferences, or permanent switchers into product UI for Unship comparisons.
- Keep inactive options safe: avoid duplicate active IDs, submit controls, global scripts, analytics triggers, focus traps, destructive side effects, and stateful providers.

## Limitations

- Unship does not decide which variant wins; the human chooses.
- Unship does not replace design review, browser QA, accessibility checks, or production release validation.
- Unship is not intended for production traffic, remote analytics, or persistent product experiments.

## Security & Safety Notes

- Run commands only in a local project the user has authorized you to modify.
- Do not run `npx @unship/cli@latest` or any unpinned remote CLI in automated agent workflows. Pin and review the package version first, then execute the project-local binary.
- Treat generated variants as temporary code that must be cleaned before release.
- Before destructive cleanup, confirm the selected option label when the user's choice is ambiguous.
- If a baseline build or typecheck already fails before Unship edits, report that baseline state and keep variant work isolated.

## Common Pitfalls

- **Problem:** Hidden variants override `hidden` with CSS.
  **Solution:** Preserve `[hidden] { display: none !important; }` near variant-specific CSS when needed.

- **Problem:** The user says "keep the second one" after more changes.
  **Solution:** Confirm the exact group and option label before editing source.

- **Problem:** The comparison grows into a broad redesign.
  **Solution:** Reduce scope to the smallest section, state, or flow that can be judged in the running app.

## Related Skills

- `@webapp-testing` - Use for browser-based functional checks after frontend changes.
- `@mobile-design` - Use when comparing mobile-specific UI patterns and platform constraints.


---
name: unslop
description: "Post-process AI-generated text through the unslop CLI to strip AI writing patterns before publishing"
category: writing
risk: safe
source: community
source_repo: MohamedAbdallah-14/unslop
source_type: community
date_added: "2026-04-25"
author: MohamedAbdallah-14
tags: [writing, content-quality, ai-writing, text-processing, cli, publishing]
tools: [claude-code, cursor, gemini-cli, codex-cli, antigravity]
license: "MIT"
license_source: "https://github.com/MohamedAbdallah-14/unslop/blob/main/LICENSE"
---

# unslop — Strip AI Writing Patterns via CLI

## Overview

unslop is a CLI tool that post-processes text to remove AI writing patterns programmatically. Unlike skills that ask the agent to avoid AI-isms, unslop runs as a deterministic pipeline step: pipe text in, get clean text out. Use it as a final pass before committing docs, publishing posts, or sending any AI-generated content to production.

The `--deterministic` flag makes output reproducible — same input always produces same output. The `--stdin` flag reads from stdin, enabling shell pipeline composition.

## When to Use This Skill

- When you have AI-generated text ready to publish and want a final cleanup pass
- When working in a shell pipeline where text quality needs to be enforced automatically
- When writing commit hooks or CI steps that validate content before it ships
- When you need reproducible text normalization across multiple runs

## Setup

Install once:

```bash
pipx install unslop
# or
uv tool install unslop
```

Verify:

```bash
unslop --version
```

## How It Works

### Step 1: Pipe Text Through unslop

Standard cleanup (may vary slightly between runs):

```bash
echo "This leverages cutting-edge AI to deliver robust solutions." | unslop --stdin
```

Deterministic cleanup (same input → same output every run):

```bash
echo "This leverages cutting-edge AI to deliver robust solutions." | unslop --stdin --deterministic
```

### Step 2: Use in Shell Pipelines

Pipe the output of any command through unslop:

```bash
cat draft.md | unslop --stdin --deterministic > clean.md
```

Or chain with other tools:

```bash
cat draft.md | unslop --stdin --deterministic | pbcopy   # macOS: copy clean text to clipboard
```

### Step 3: Integrate into Commit Hooks or CI

Add to a pre-commit hook or CI step to enforce quality gates on any generated content before it ships:

```bash
# In .git/hooks/pre-commit or a CI script
CONTENT=$(cat docs/changelog.md)
CLEANED=$(echo "$CONTENT" | unslop --stdin --deterministic)
if [ "$CONTENT" != "$CLEANED" ]; then
  echo "Changelog contains AI writing patterns. Run: cat docs/changelog.md | unslop --stdin --deterministic > docs/changelog.md"
  exit 1
fi
```

## Examples

### Example 1: Clean a Draft Document

```bash
cat blog-post-draft.md | unslop --stdin --deterministic > blog-post-final.md
```

### Example 2: Inline Cleanup During Writing

```bash
# Write content, pipe through unslop, write result back
cat README.md | unslop --stdin > README.clean.md && mv README.clean.md README.md
```

### Example 3: Validate Before Submitting a PR

```bash
# Check if any generated docs need cleanup
for f in docs/*.md; do
  ORIGINAL=$(cat "$f")
  CLEANED=$(echo "$ORIGINAL" | unslop --stdin --deterministic)
  [ "$ORIGINAL" != "$CLEANED" ] && echo "Needs cleanup: $f"
done
```

## Best Practices

- ✅ Use `--deterministic` in CI and automation to ensure reproducible output
- ✅ Run on the final draft, not intermediate iterations
- ✅ Combine with the `avoid-ai-writing` skill for both generation-time guidance and post-processing
- ❌ Don't run on code files — unslop targets prose, not source code
- ❌ Don't skip review after unslop: automated cleanup can occasionally change meaning; read the output

## Limitations

- Processes prose only — not code, JSON, or structured data
- Does not catch factual errors or substantive writing issues
- Some replacements may not fit every context; review the output before publishing
- Requires Python tooling such as `pipx` or `uv` for standalone CLI installation

## Security & Safety Notes

- unslop reads from stdin and writes to stdout — no file system side effects by default
- `--deterministic` mode is local and does not make LLM API calls
- Default LLM mode may use `ANTHROPIC_API_KEY` or the Claude CLI; use `--deterministic` for sensitive local files and CI gates
- Safe to run in CI pipelines and commit hooks when pinned to deterministic mode


---
name: unslop-commit
description: Rewrites commit messages so they sound like a careful human engineer wrote them. Strips AI/marketing slop ("comprehensive solution", "robust implementation", "leverage", "enhance", "seamlessly", "This commit..."). Keeps Conventional Commits format. Subject ≤72 chars (aim ≤50),...
risk: critical
source: https://github.com/MohamedAbdallah-14/unslop/tree/main/plugins/unslop/skills/unslop-commit
source_repo: MohamedAbdallah-14/unslop
source_type: community
date_added: 2026-07-01
license: MIT
license_source: https://github.com/MohamedAbdallah-14/unslop/blob/main/LICENSE
---

# Unslop Commit
## When to Use

Use this skill when you need rewrites commit messages so they sound like a careful human engineer wrote them. Strips AI/marketing slop ("comprehensive solution", "robust implementation", "leverage", "enhance", "seamlessly", "This commit..."). Keeps Conventional Commits format. Subject ≤72 chars (aim ≤50),...

## Purpose

Generate or rewrite commit messages so they read like a real engineer wrote them at the end of a real day. Conventional Commits format. Direct, specific, no template English. Why over what.

## Trigger

`/unslop-commit`, `/commit`, "write a commit", "commit message", "humanize this commit", "de-slop this commit". Auto-trigger when the user has staged changes and asks for a commit message.

## Rules

### Subject line

- Format: `<type>(<scope>): <imperative summary>`
- Scope optional. Types: `feat`, `fix`, `chore`, `refactor`, `docs`, `test`, `perf`, `build`, `ci`, `revert`.
- Imperative mood: `add`, `fix`, `move`, `remove` — not `added`, `fixes`, `fixing`.
- ≤50 chars when possible. Hard cap 72.
- No trailing period.
- Lowercase after `:` unless the project capitalizes.

### Body (only when subject can't carry it)

- Add for: non-obvious "why", breaking changes, migrations, security context, data integrity.
- Wrap at 72 chars. Bullets `-` for two or more independent points. Single paragraph for one thought.
- End with refs: `Closes #42`, `Refs #17`. No `BREAKING CHANGE:` unless truly breaking — and then write it.

### Never include

- Template prefixes: "This commit...", "This change...", "We are...", "I have..."
- Marketing verbs: comprehensive, robust, enhance, leverage, seamless, holistic
- Filler adverbs: just, really, basically, simply, actually
- Restating the filename when scope already names it
- "As requested by..." (use `Co-authored-by:` if you need attribution)
- AI attribution unless the project requires it
- Emoji unless project convention says so

### Auto-clarity (always include body)

- Breaking changes
- Security fixes
- Data migrations
- Reverts (cite the reverted commit)

## Examples

### Bad → good (slop subject, no body)

- Bad: `feat: implement a comprehensive, robust solution for user profile retrieval with enhanced error handling`
- Good: `feat(api): return profile fields the mobile client actually needs`

### Bad → good (vague body)

Bad:
```
fix: fixed the bug

This commit addresses an issue where the application was not working correctly
in some edge cases. We've improved the logic to handle these scenarios.
```

Good:
```
fix(checkout): ignore stale cart id from localStorage

Stale cart ids came from tabs that hadn't refreshed after a deploy. Server
now treats unknown ids as empty cart instead of 500.

Closes #842
```

### Breaking change

```
feat(api)!: rename /v1/orders to /v1/customer-orders

The old route stays in place until the next major release but logs a
deprecation warning. Internal services have been migrated.

BREAKING CHANGE: third-party integrations using /v1/orders directly need
to switch to /v1/customer-orders by 2026-07-01.

Closes #1290
```

## Boundaries

- Output the message only, in a single fenced block, ready to paste.
- Do not run `git commit`, stage, or amend.
- If the change is genuinely trivial (`docs(readme): fix typo`), keep it trivial. Don't pad.
- Never invent context the user didn't provide. If the "why" isn't clear, ask, or omit the body.

## Limitations

- Use this skill only when the task clearly matches its upstream source and local project context.
- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.


---
name: unslop-file
description: "Humanize natural-language memory files (CLAUDE.md, todos, preferences, docs) by removing AI-isms and adding burstiness while preserving every code block, URL, path, command, and heading exactly. Two modes: --deterministic (fast, regex-based, no API) and LLM (default, calls Claude for..."
risk: critical
source: https://github.com/MohamedAbdallah-14/unslop/tree/main/plugins/unslop/skills/unslop-file
source_repo: MohamedAbdallah-14/unslop
source_type: community
date_added: 2026-07-01
license: MIT
license_source: https://github.com/MohamedAbdallah-14/unslop/blob/main/LICENSE
---

# Unslop Humanize
## When to Use

Use this skill when you need humanize natural-language memory files (CLAUDE.md, todos, preferences, docs) by removing AI-isms and adding burstiness while preserving every code block, URL, path, command, and heading exactly. Two modes: --deterministic (fast, regex-based, no API) and LLM (default, calls Claude for...

## Purpose

Rewrite natural-language memory files (CLAUDE.md, AGENTS.md, todos, preferences, docs) so they sound human-written: no sycophancy, no stock vocab, no five-paragraph essay shape, no tricolon padding. Everything technical stays exact: code blocks, inline code, URLs, file paths, commands, headings, tables.

Two modes:

- **`--deterministic`** — fast regex pass that strips canonical AI-isms and tightens tricolons. No API call, no `ANTHROPIC_API_KEY` needed. Best for batch processing and CI.
- **LLM mode (default)** — calls Claude (via Anthropic SDK or `claude --print` CLI fallback) to do a full rewrite that engineers burstiness, restructures performative paragraphs, and matches voice. Slower but better quality.

Humanized version overwrites the original. A `FILE.original.md` backup is written first. Re-run after editing the `.original.md` to regenerate.

### Intensity levels (`--mode`)

| Mode       | What runs                                                                                | Use when…                                                    |
| ---------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `subtle`   | Stock vocab only.                                                                           | Structure is fine; you just want AI vocabulary gone.         |
| `balanced` | (Default.) Sycophancy, hedging, transitions, stock vocab, authority tropes, signposting, performative balance, em-dash cap. | Everyday docs / READMEs / CLAUDE.md.                         |
| `full`     | Balanced + filler phrases + negative-parallelism tricolons + stronger LLM prompt.           | Marketing copy, release notes, slop-heavy LLM output.        |

### Two-pass audit

Use the deterministic pass to get a report, then fix anything that slipped:

```bash
humanize --deterministic --report audit.json doc.md     # writes audit + humanized
humanize doc.md                                         # optional LLM polish on top
```

`audit.json` lists every rule that fired, every `before → after` pair, and `counts_by_rule`. Great for reviewing what the regex changed before trusting the diff to merge.

## Trigger

`/unslop-file <filepath>`, `/unslop:humanize <filepath>`, or "humanize memory file", "de-slop this doc", "strip AI tone from this file".

## Process

The scripts live in a `scripts/` directory adjacent to this SKILL.md.

Common layouts:
- Full repo: `unslop/SKILL.md` + `unslop/scripts/`
- Synced mirror: `skills/unslop-file/SKILL.md` + `skills/unslop-file/scripts/`
- Codex bundle: `plugins/unslop/skills/unslop-file/SKILL.md` + sibling `scripts/`

Always prefer the `scripts/` sibling of the currently loaded SKILL file.

Steps:

1. Locate the directory containing this SKILL.md and its `scripts/` sibling.
2. Run from that directory: `python3 -m scripts <absolute_filepath>` (LLM mode), or add `--deterministic` for the regex pass.
3. CLI flow: detect file type → write `.original.md` backup → humanize → validate (preserve check + AI-ism residual check) → on validation error: targeted fix call (LLM mode) → retry up to 2 times.
4. On final failure: report errors, restore original, exit 2.
5. On success: report path of humanized file and `.original.md` backup, exit 0.
6. Return result to user.

## Humanization Rules

### Remove (canonical AI-isms)

- **Sycophancy openers**: "Great question!", "Certainly!", "Absolutely!", "Sure!", "I'd be happy to help", "What a fascinating..."
- **Stock vocab**: `delve`, `tapestry`, `testament` (praise form), `navigate`/`embark`/`journey` (figurative), `realm`, `landscape` (figurative), `pivotal`, `paramount`, `seamless`, `holistic`, `leverage` (filler verb), `robust` (filler), `comprehensive` (when "complete" works), `cutting-edge`, `state-of-the-art` (filler), `interplay`, `intricate`, `vibrant`, `underscore(s)/d/ing` (figurative), `crucial`, `vital` (role/importance/part), `ever-evolving`, `ever-changing`, `in today's (digital) world/age`, `dynamic landscape`.
- **Hedging openers**: "It's important to note that", "It's worth mentioning", "Generally speaking", "In essence", "At its core", "It should be noted that", "It's also worth pointing out".
- **Authority tropes** (sentence start): "At its core,", "In reality,", "Fundamentally,", "What really matters is", "The heart of the matter is", "At the heart of X is/lies".
- **Signposting announcements**: "Let's dive in(to ...)", "Let's break this down", "Here's what you need to know", "Without further ado", "In this article, I'll ...", "Buckle up".
- **Transition tics** (sentence start): "Furthermore,", "Moreover,", "Additionally,", "In conclusion,", "To summarize,".
- **Performative balance**: "however" / "on the other hand" appended to every claim.
- **Em-dash pileups** (more than two em-dashes per paragraph).
- **Filler phrases** (`--mode full` only): "in order to" → "to", "due to the fact that" → "because", "prior to" → "before", "with regard to" → "about", "a wide variety of" → "many", "at this point in time" → "now", "the fact that" → "that", etc.
- **Negative-parallelism tricolons** (`--mode full` only): "No guesswork, no bloat, no surprises." — the rhetorical triple-no punch.

### Tighten

- Tricolons: "X, Y, and Z" stacks where two would suffice — keep two, drop the weakest
- Bullet soup: three bullets that say the same thing → merge into one sentence
- Five-paragraph essay shapes: vary paragraph length; don't write four paragraphs of identical length

### Preserve EXACTLY (never modify)

- Fenced code blocks (```...```) — every byte
- Indented code blocks (4-space)
- Inline code (`...`)
- URLs and markdown links
- File paths (`./src/`, `/etc/`, `C:\Users\...`)
- Commands (`npm install`, `git rebase`, `docker run`)
- Technical terms, proper nouns, API names
- Dates, version numbers, numerics
- Environment variables (`$HOME`, `${NODE_ENV}`)

### Preserve structure

- All markdown headings (text exact)
- Bullet hierarchy and nesting
- Numbered lists
- Tables (compress cells; keep structure)
- YAML frontmatter

### CRITICAL RULE

Everything inside ` ``` ... ``` ` is read-only. No comment changes, no whitespace changes, no line reordering. Inline backticks: same. Code is the substrate; humanization only operates on prose between code regions.

## Pattern (before → after)

| #   | Before                                                                                | After (deterministic, `--mode balanced`)                                               |
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| 1   | It's important to note that running tests prior to pushing changes is a comprehensive best practice. Additionally, it's worth mentioning that this can prevent broken builds.                                         | Running tests before pushing changes is a broad best practice. This can prevent broken builds. |
| 2   | The application leverages a microservices architecture that comprises multiple discrete components.                                                                                | The application uses a microservices architecture that comprises multiple discrete components. |
| 3   | At its core, caching trades memory for latency.                                                                                | Caching trades memory for latency.                                                     |
| 4   | Let's dive in. Here is the first step.                                                                                | Here is the first step.                                                                |
| 5   | The intricate interplay between caching and latency is crucial.                                                                                | The detailed link between caching and latency is important.                            |
| 6   | In today's digital world, we ship fast.                                                                                | Today, we ship fast.                                                                   |

### At `--mode full`, additionally:

| #   | Before                                                   | After                                 |
| --- | -------------------------------------------------------- | ------------------------------------- |
| 7   | We ran the tests in order to verify the fix.             | We ran the tests to verify the fix.   |
| 8   | The build failed due to the fact that the disk was full. | The build failed because the disk was full. |
| 9   | No guesswork, no bloat, no surprises.                    | _(stripped)_                          |

### Reference

- `blader/unslop` — Claude-Code skill listing 30+ AI tells; we incorporated the strongest signals.
- Wikipedia: *Signs of AI writing* — public taxonomy cross-referenced for vocab.
- Full comparison + gap analysis: `docs/research/IMPLEMENTATION_TRACE.md`.

## Boundaries

- Only operate on `.md`, `.txt`, `.markdown`, `.rst`, or extensionless natural language.
- Never modify `.py`, `.js`, `.ts`, `.json`, `.yaml`, `.yml`, `.toml`, `.env`, `.lock`, `.css`, `.html`, `.xml`, `.sql`, `.sh`.
- Mixed prose-and-code files: humanize only the prose; leave fenced code untouched.
- If unsure whether a file is prose or code: leave unchanged.
- Backup `FILE.original.md` is written before overwrite. Never humanize a file already named `*.original.md`.
- Sensitive paths (anything matching `.env*`, `*.pem`, `*.key`, `~/.ssh/`, `~/.aws/`, etc.) are refused before any read or API call.
- Files larger than 500 KB are refused.

## Limitations

- Use this skill only when the task clearly matches its upstream source and local project context.
- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.


---
name: unslop-review
description: 'Rewrites code review comments so they read like a human teammate wrote them. Cuts corporate-AI throat-clearing ("I noticed...", "I was wondering if perhaps...", "It might be worth considering..."). Each comment is direct: location, the issue, a concrete fix. Use when user says...'
risk: critical
source: https://github.com/MohamedAbdallah-14/unslop/tree/main/plugins/unslop/skills/unslop-review
source_repo: MohamedAbdallah-14/unslop
source_type: community
date_added: 2026-07-01
license: MIT
license_source: https://github.com/MohamedAbdallah-14/unslop/blob/main/LICENSE
---

# Unslop Review
## When to Use

Use this skill when you need rewrites code review comments so they read like a human teammate wrote them. Cuts corporate-AI throat-clearing ("I noticed...", "I was wondering if perhaps...", "It might be worth considering..."). Each comment is direct: location, the issue, a concrete fix. Use when user says...

## Purpose

Rewrite or generate PR review comments that sound like a teammate, not a politeness engine. Direct on the issue, concrete on the fix, kind on the human.

## Trigger

`/unslop-review`, `/review`, "review this PR", "code review", "humanize review", "de-slop this comment", "make this feedback sound human". Auto-trigger when reviewing pull requests.

## Format

Default shape: `L<line>: <severity prefix> <observation>. <fix>.`

Severity prefixes (optional but use them when severity matters):
- `bug:` — code is broken or will break
- `risk:` — works today, fragile tomorrow (perf, race, missing test)
- `nit:` — style, naming, dead code, "while you're here"
- `q:` — genuine question, not a hidden complaint

Multi-file: `<file>:L<line>: <severity> <observation>. <fix>.`

Range: `L88-140: ...` when the issue spans lines.

## Rules

### Drop

- Throat-clearing: "I noticed that...", "It seems like...", "It looks like to me..."
- Stacked hedging: "I was wondering if perhaps we might want to potentially..."
- Polite-padding: "I would kindly suggest...", "just a small suggestion..."
- Per-comment praise: "Nice work on this function but...", "Great pattern, however..."
- Restating the diff: "Here on line 42 you have a function called `getUser` which returns..."
- Bare opinion without a fix: "This is bad" with no suggestion

### Keep

- Exact line numbers and ranges
- Identifiers in backticks: `findUser`, `req.body.id`
- Concrete fix or concrete question
- "Why" only when the fix isn't obvious

### Tone

Human, not corporate. "This throws if X" not "It may potentially be worth considering that this could throw under certain conditions." Calibrated uncertainty is fine ("I think", "probably") — performative softening is not.

### Auto-clarity (use full prose, not one-liners)

- Security findings (CVE-class, auth, secrets)
- Architecture disagreements that need a real discussion
- Onboarding context for a new contributor
- When the answer is genuinely "this is fine"

In those cases use a short paragraph, then resume terse for the rest.

## Examples

### Bad → good

- Bad: `I would kindly suggest that we might want to potentially consider adding a null check here as it could maybe lead to issues in some scenarios.`
- Good: `L42: bug: \`findUser\` returns undefined when no match. Guard before \`user.email\` or early-return 404.`

- Bad: `Great work on this implementation! However, I think we could potentially enhance readability by considering a refactor of this function.`
- Good: `L88-140: nit: this function does validation, I/O, and mapping. Splitting them would make the happy path easier to follow. Happy to pair on a cut if helpful.`

- Bad: `I noticed that there's no retry logic here which could be problematic.`
- Good: `L23: risk: no retry on 429. Wrap the call in \`withBackoff(3)\` so we don't drop legitimate requests.`

- Bad: `This implementation leverages a robust caching strategy.`
- Good: (delete — empty praise. If the caching is genuinely interesting, explain why specifically.)

### Approval

If the change is solid and you have nothing concrete: `LGTM` on its own line. No boilerplate.

## Boundaries

- Comments only. No commits, no `git push`, no auto-approve, no linter runs.
- Output is paste-ready: one comment per line, or a clearly separated list.
- Severity must be honest. Don't downgrade a `bug` to a `nit` to soften the message.

## Limitations

- Use this skill only when the task clearly matches its upstream source and local project context.
- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.


---
name: unsloth-finetuning
description: "Fine-tune and post-train LLMs with Unsloth Core on a single consumer GPU: VRAM sizing, LoRA/QLoRA, GRPO/DPO, chat-template correctness, and GGUF export."
category: ai-ml
risk: critical
source: community
source_repo: unslothai/unsloth
source_type: community
date_added: "2026-08-27"
author: A-ryanVAT-S
tags: [unsloth, fine-tuning, lora, qlora, grpo, gguf, vram]
tools: [claude, cursor, gemini]
license: "Apache-2.0"
license_source: "https://github.com/unslothai/unsloth/blob/main/LICENSE"
---

# Unsloth Fine-Tuning

## Overview

Unsloth trains LLMs with custom kernels that cut VRAM use and step time without changing the
math, which makes single-GPU fine-tuning practical on hardware that would otherwise OOM.
This skill covers **Unsloth Core** — the Python API — because that is what an agent can drive
programmatically; the Desktop app and Studio web UI are interactive and out of scope.

The hard parts of an Unsloth run are not the training call. They are sizing the job against
available VRAM, getting the chat template and loss masking right, and choosing an export
format the target runtime can actually load. This skill covers those three.

## When to Use This Skill

- Use when fine-tuning an LLM on one GPU and VRAM is the binding constraint.
- Use when a training run OOMs and needs to be resized rather than rewritten.
- Use when doing preference or RL post-training (GRPO, DPO) on consumer hardware.
- Use when a fine-tuned model must be exported to GGUF, vLLM, or merged 16-bit weights.
- Use when a fine-tune "ran fine" but the model's output format is wrong — usually a chat
  template or loss-masking bug, not a hyperparameter one.

### Do not use this skill when

- The training is multi-node or large-scale multi-GPU. Use plain TRL with Accelerate/DeepSpeed.
- The architecture is unsupported by Unsloth. Fall back to TRL; do not force it.
- The user wants managed cloud training. That is Hugging Face Jobs, not local Unsloth.
- The user wants the Desktop or Studio GUI. Point them at the installer, not this skill.

## How It Works

### Step 1: Size the run before writing code

VRAM is the constraint that decides everything else. Estimate weights first, then leave room
for activations and optimizer state:

| Load mode | Weight cost | 8B model | Use when |
| :--- | :--- | :--- | :--- |
| `load_in_4bit` (QLoRA) | ~0.55 GB per 1B params | ~4.5 GB | Default. Under 16 GB VRAM. |
| `load_in_8bit` | ~1.1 GB per 1B params | ~9 GB | Quality-sensitive, 16-24 GB. |
| `load_in_16bit` | ~2 GB per 1B params | ~16 GB | LoRA at full precision, 24 GB+. |
| `full_finetuning=True` | ~2 GB weights + ~12 GB optimizer | ~112 GB | Rarely justified. Prefer LoRA. |

Add roughly 2-6 GB for activations, scaling with `max_seq_length` and batch size. Treat these
as planning figures and confirm against `nvidia-smi` on the first run — they vary by
architecture, attention implementation and vocabulary size.

If the estimate does not fit, reduce in this order: `max_seq_length`, then batch size (raising
`gradient_accumulation_steps` to hold the effective batch constant), then LoRA rank, then model
size. Cutting rank before sequence length usually costs more quality than it saves memory.

### Step 2: Load the model

`import unsloth` must come **before** `transformers`, `trl` or `peft`. Unsloth patches those
libraries at import time; importing them first silently disables the optimizations.

```python
import unsloth  # must be first
from unsloth import FastLanguageModel

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen3-8B",
    max_seq_length = 2048,
    load_in_4bit = True,
    dtype = None,  # auto-detects bf16 where supported
)
```

Pick the loader that matches the modality: `FastLanguageModel` for text-only causal LMs,
`FastVisionModel` for vision-language models, `FastModel` when the modality is decided at runtime.

The `unsloth/` Hub namespace holds pre-quantized copies that download faster and skip a local
quantization pass. Upstream repos such as `Qwen/` or `meta-llama/` work identically.

### Step 3: Fix the chat template before training

This is the most common silent failure. A run with the wrong template converges cleanly and
produces a model that ignores its stop tokens or emits prompt scaffolding at inference.

```python
from unsloth.chat_templates import (
    get_chat_template,
    standardize_data_formats,
    train_on_responses_only,
)

tokenizer = get_chat_template(tokenizer, chat_template = "qwen3")
dataset = standardize_data_formats(dataset)  # normalizes ShareGPT/OpenAI column names
```

Then mask the prompt so loss is computed on assistant turns only. Without this, the model is
also trained to generate user messages:

```python
trainer = train_on_responses_only(
    trainer,
    instruction_part = "<|im_start|>user\n",
    response_part = "<|im_start|>assistant\n",
)
```

The two part strings must match the template's actual delimiters. Verify by decoding one batch
and confirming the masked region covers exactly the prompt.

### Step 4: Attach LoRA adapters

```python
model = FastLanguageModel.get_peft_model(
    model,
    r = 16,
    lora_alpha = 16,
    lora_dropout = 0.0,
    target_modules = [
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    use_gradient_checkpointing = "unsloth",  # Unsloth's variant, lower VRAM than True
    random_state = 3407,
)
```

Rank guidance: `r=8-16` for style and format adaptation, `r=32-64` when teaching genuinely new
capability. Setting `lora_alpha` to 1-2x `r` is a safe default. Keep `lora_dropout = 0.0` —
Unsloth's fast path is only taken when dropout is zero.

Train all seven projection modules unless VRAM forces otherwise; attention-only LoRA
underperforms noticeably on instruction data. For MoE models, expert layers are `nn.Parameter`
rather than `nn.Linear` and need `target_parameters` instead of `target_modules`.

### Step 5: Train

Unsloth returns standard PEFT-wrapped models, so TRL's trainers work unmodified.

```python
from trl import SFTTrainer, SFTConfig

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset,
    args = SFTConfig(
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 8,  # effective batch 16
        warmup_steps = 5,
        num_train_epochs = 1,
        learning_rate = 2e-4,
        optim = "adamw_8bit",
        output_dir = "outputs",
    ),
)
trainer.train()
```

`2e-4` suits LoRA; full fine-tuning needs roughly 10x lower. One to three epochs is typical —
LoRA overfits small datasets quickly, so watch eval loss rather than trusting an epoch count.

### Step 6: Export to the target runtime

The right format depends entirely on where the model will run:

| Target | Call | Notes |
| :--- | :--- | :--- |
| llama.cpp / Ollama / LM Studio | `model.save_pretrained_gguf(dir, tokenizer, quantization_method="q4_k_m")` | Builds llama.cpp on first use. |
| vLLM / TGI / Transformers | `model.save_pretrained_merged(dir, tokenizer, save_method="merged_16bit")` | Full-size weights. |
| Adapter only (swapped at runtime) | `model.save_pretrained_merged(dir, tokenizer, save_method="lora")` | Megabytes, not gigabytes. |
| Hugging Face Hub | `model.push_to_hub_gguf(...)` / `model.push_to_hub_merged(...)` | Needs a write token. |

`quantization_method` accepts a list, so several GGUF quants can be produced in one conversion
pass: `["q4_k_m", "q5_k_m", "q8_0"]`. `q4_k_m` is the usual quality/size compromise. The `iq*`
importance-matrix quants additionally require `imatrix_file=`.

Avoid `save_method="merged_4bit"` for anything redistributed — it bakes in the quantization and
cannot be cleanly re-quantized afterwards.

## Examples

### Example 1: QLoRA SFT on a 16 GB GPU

```python
import unsloth
from unsloth import FastLanguageModel
from unsloth.chat_templates import get_chat_template, train_on_responses_only
from datasets import load_dataset
from trl import SFTTrainer, SFTConfig

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen3-8B",
    max_seq_length = 2048,
    load_in_4bit = True,
)
model = FastLanguageModel.get_peft_model(model, r = 16, lora_alpha = 16)

tokenizer = get_chat_template(tokenizer, chat_template = "qwen3")
dataset = load_dataset("mlabonne/FineTome-100k", split = "train[:5000]")

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset,
    args = SFTConfig(
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 8,
        num_train_epochs = 1,
        learning_rate = 2e-4,
        optim = "adamw_8bit",
        output_dir = "outputs",
    ),
)
trainer = train_on_responses_only(
    trainer,
    instruction_part = "<|im_start|>user\n",
    response_part = "<|im_start|>assistant\n",
)
trainer.train()

model.save_pretrained_gguf("qwen3-tuned", tokenizer, quantization_method = "q4_k_m")
```

### Example 2: GRPO with vLLM-backed generation

GRPO samples several completions per prompt at every step, so generation dominates step time.
Load with `fast_inference=True` to route sampling through vLLM in the same process.

```python
import unsloth
from unsloth import FastLanguageModel
from trl import GRPOTrainer, GRPOConfig

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen3-4B",
    max_seq_length = 1024,
    load_in_4bit = True,
    fast_inference = True,       # vLLM sampling backend
    max_lora_rank = 32,          # must be >= the r used below
    gpu_memory_utilization = 0.6,
)
model = FastLanguageModel.get_peft_model(model, r = 32, lora_alpha = 32)

def reward_length(completions, **kwargs):
    """Placeholder. Replace with a task-specific verifier."""
    return [min(len(c) / 200.0, 1.0) for c in completions]

trainer = GRPOTrainer(
    model = model,
    processing_class = tokenizer,
    reward_funcs = [reward_length],
    train_dataset = dataset,
    args = GRPOConfig(
        num_generations = 8,
        max_prompt_length = 256,
        max_completion_length = 512,
        learning_rate = 5e-6,
        output_dir = "grpo-outputs",
    ),
)
trainer.train()
```

`gpu_memory_utilization` splits VRAM between vLLM's KV cache and training. Raise it if
generation is the bottleneck, lower it if training OOMs. `max_lora_rank` is fixed at load time
and must be at least the `r` passed later, or adapter loading fails.

GRPO learning rates sit roughly two orders of magnitude below SFT. Reward functions receive
`completions` plus any dataset columns as keyword arguments, and return one float per completion.

## Best Practices

- ✅ Set `random_state` so a promising run can be reproduced.
- ✅ Log peak VRAM on the first run and reuse it to size later jobs on the same hardware.
- ✅ Evaluate the exported artifact, not just the adapter — quantization shifts behaviour.
- ❌ Don't change `max_seq_length` between training and export; the GGUF inherits it.
- ❌ Don't tune hyperparameters before the loss mask has been verified once.

## Limitations

- The VRAM figures above are planning heuristics, not benchmarks. Confirm on target hardware.
- Architecture support changes between releases. Check upstream before assuming a model works.
- Unsloth's speed and memory claims are the project's own published figures, measured on their
  own benchmarks; they are not independently verified here.
- This skill does not replace environment-specific validation, testing, or expert review.
- Stop and ask for clarification if the GPU, model, dataset format or export target is unknown —
  every step above depends on those four.

## Security & Safety Notes

- Training commands are long-running and hold the GPU exclusively. Confirm before launching on
  a shared or remote machine.
- `push_to_hub_gguf` and `push_to_hub_merged` publish weights to a public Hub repo by default.
  Confirm intent and pass `private=True` when the model is not meant to be public.
- Read Hugging Face tokens from the environment (`HF_TOKEN`), never inline in a script. A
  committed token grants write access to every model the account owns.
- Fine-tuning reproduces the training data's content and biases in the weights. Confirm the
  dataset is licensed for training and free of secrets before starting.
- GGUF export builds llama.cpp from source on first use, compiling third-party code and
  requiring network access. Expect it to be slow and to need a working toolchain.
- Unsloth is dual-licensed: the core package is Apache-2.0, while optional components such as
  the Studio UI are AGPL-3.0. Check a component's license before redistributing it.

## Common Pitfalls

- **Problem:** Trained model ignores stop tokens or echoes the prompt format.
  **Solution:** Wrong chat template, or `train_on_responses_only` was never applied. Verify the
  mask on a decoded batch before blaming hyperparameters.

- **Problem:** CUDA OOM partway through the first epoch rather than at step 0.
  **Solution:** A long sample exceeded the activation budget. Lower `max_seq_length` or filter
  outliers — peak memory tracks the longest sequence, not the mean.

- **Problem:** Training runs, but at ordinary unaccelerated speed.
  **Solution:** `transformers` or `trl` was imported before `unsloth`, so the patches never
  applied. Move `import unsloth` to the top of the file.

- **Problem:** `save_pretrained_gguf` appears to hang on first call.
  **Solution:** It is building llama.cpp. Ensure a compiler and network access are available, or
  export `merged_16bit` and convert separately.

- **Problem:** GRPO fails with a LoRA rank mismatch.
  **Solution:** `max_lora_rank` at `from_pretrained` is below the `r` given to `get_peft_model`.
  Raise it to match.

- **Problem:** Loss collapses to near zero within a few hundred steps.
  **Solution:** Overfitting a small dataset, or the loss mask is leaking the answer into the
  prompt. Check dataset size against epoch count, then re-verify masking.

## Related Skills

- `@trl-training` - Use for the TRL CLI, multi-GPU runs, or architectures Unsloth lacks.
- `@hugging-face-model-trainer` - Use for managed training on Hugging Face Jobs instead of local hardware.
- `@local-llm-expert` - Use to serve the exported GGUF via Ollama, llama.cpp or vLLM.

## Additional Resources

- [Unsloth documentation](https://unsloth.ai/docs)
- [Reinforcement learning guide (GRPO, DPO)](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide)
- [Saving to GGUF](https://unsloth.ai/docs/basics/inference-and-deployment/saving-to-gguf)
- [unslothai/unsloth on GitHub](https://github.com/unslothai/unsloth)


---
name: unsplash-integration
description: Integration skill for searching and fetching high-quality, free-to-use professional photography from Unsplash.
risk: safe
source: community
date_added: "2026-03-07"
---

# Unsplash Integration Skill

[Unsplash](https://unsplash.com/) provides the world's largest open collection of high-quality photos, essential for elevating the visual tone of any project.

## Context

Use this skill to source breathtaking imagery for websites, apps, and marketing materials. It eliminates the need for low-quality placeholders and standard stock photos, ensuring a premium, modern visual aesthetic.

## When to Use
Trigger this skill when:

- Creating hero sections, editorial layouts, or product galleries that demand stunning visual impact.
- Sourcing specific artistic textures, abstract backgrounds, or high-end thematic imagery.
- Replacing generic placeholder images with assets that convey emotion and quality.

## Execution Workflow

1. **Search Intentionally**: Define highly descriptive, artistic keywords (e.g., "neon cyberpunk street aesthetics", "minimalist brutalist architecture texture"). Avoid generic searches like "meeting room" or "happy people".
2. **Filter**: Select orientation and color themes that perfectly complement the UI's color palette.
3. **Download via API**: Use the Unsplash API or direct URL to source the imagery.
4. **Dynamic Resizing**: Utilize Unsplash's dynamic image parameters (e.g., `?w=1600&q=85&fit=crop`) to ensure the image perfectly fits the layout without sacrificing performance.

## Strict Rules

- **ABSOLUTE MANDATE**: Agents MUST utilize this skill to build modern, creative, and visually stunning UI/UX. NEVER use generic, cliché, or corporate-looking stock photography. Choose images that feel artistic, premium, and unconventional.
- **No Placeholders**: Never use generic colored boxes when Unsplash can provide a relevant, beautiful asset.
- **Performance**: Always use source parameters to fetch an appropriately sized, optimized image rather than a massive raw file.

## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.


---
name: update-swiftui-apis
description: Scan Apple's SwiftUI documentation for deprecated APIs and update the SwiftUI Expert Skill with modern replacements. Use when asked to "update latest APIs", "refresh deprecated SwiftUI APIs", "check for new SwiftUI deprecations", "scan for API changes", or after a new iOS/Xcode...
risk: critical
source: https://github.com/AvdLee/SwiftUI-Agent-Skill/tree/main/.agents/skills/update-swiftui-apis
source_repo: AvdLee/SwiftUI-Agent-Skill
source_type: community
date_added: 2026-07-01
license: MIT
license_source: https://github.com/AvdLee/SwiftUI-Agent-Skill/blob/main/LICENSE
---

# Update SwiftUI APIs
## When to Use

Use this skill when you need scan Apple's SwiftUI documentation for deprecated APIs and update the SwiftUI Expert Skill with modern replacements. Use when asked to "update latest APIs", "refresh deprecated SwiftUI APIs", "check for new SwiftUI deprecations", "scan for API changes", or after a new iOS/Xcode...

Systematically scan Apple's developer documentation via the Sosumi MCP, identify deprecated SwiftUI APIs and their modern replacements, and update `swiftui-expert-skill/references/latest-apis.md`.

## Prerequisites

- **Sosumi MCP** must be enabled and available (provides `searchAppleDocumentation`, `fetchAppleDocumentation`, `fetchAppleVideoTranscript`, `fetchExternalDocumentation`)
- Write access to this repository (or a fork)

## Workflow

### 1. Understand current coverage

Read `swiftui-expert-skill/references/latest-apis.md` to understand:
- Which deprecated-to-modern transitions are already documented
- The version segments in use (iOS 15+, 16+, 17+, 18+, 26+)
- The Quick Lookup Table at the bottom

### 2. Load the scan manifest

Read `references/scan-manifest.md` (relative to this skill). It contains the categorized list of API areas, documentation paths, search queries, and WWDC video paths to scan.

### 3. Scan Apple documentation

For each category in the manifest:

1. Call `searchAppleDocumentation` with the listed queries to discover relevant pages.
2. Call `fetchAppleDocumentation` with specific documentation paths to get full API details.
3. Look for deprecation notices, "Deprecated" labels, and "Use ... instead" guidance.
4. Note the iOS version where the modern replacement became available.
5. Optionally call `fetchAppleVideoTranscript` for WWDC sessions that announce API changes.

Batch related searches together for efficiency. Focus on finding **new** deprecations not yet in `latest-apis.md`.

### 4. Compare and identify changes

Compare findings against existing entries. Categorize results:
- **New deprecations**: APIs not yet documented in `latest-apis.md`
- **Corrections**: Existing entries that need updating (wrong version, better replacement available)
- **New version segments**: If a new iOS version introduces deprecations, add a new section

### 5. Update latest-apis.md

Follow the established format exactly. Each entry must include:

**Section placement** -- place under the correct version segment:
- "Always Use (iOS 15+)" for long-deprecated APIs
- "When Targeting iOS 16+" / "17+" / "18+" / "26+" for version-gated changes

**Entry format:**

```markdown
**Always use `modernAPI()` instead of `deprecatedAPI()`.**

\```swift
// Modern
View()
    .modernAPI()

// Deprecated
View()
    .deprecatedAPI()
\```
```

**Quick Lookup Table** -- add a row at the bottom of the file:

```markdown
| `deprecatedAPI()` | `modernAPI()` | iOS XX+ |
```

Keep the attribution line at the top of the file:
> Based on a comparison of Apple's documentation using the Sosumi MCP, we found the latest recommended APIs to use.

### 6. Open a pull request

1. Create a branch from `main` named `update/latest-apis-YYYY-MM` (use current year and month).
2. Commit changes to `swiftui-expert-skill/references/latest-apis.md`.
3. Open a PR via `gh pr create` with:
   - **Title**: "Update latest SwiftUI APIs (Month Year)"
   - **Body**: Summary of new/changed entries, attribution to Sosumi MCP

## Sosumi MCP Tool Reference

| Tool | Parameters | Returns |
|------|-----------|---------|
| `searchAppleDocumentation` | `query` (string) | JSON with `results[]` containing `title`, `url`, `description`, `breadcrumbs`, `tags`, `type` |
| `fetchAppleDocumentation` | `path` (string, e.g. `/documentation/swiftui/view/foregroundstyle(_:)`) | Markdown documentation content |
| `fetchAppleVideoTranscript` | `path` (string, e.g. `/videos/play/wwdc2025/10133`) | Markdown transcript |
| `fetchExternalDocumentation` | `url` (string, full https URL) | Markdown documentation content |

## Tips

- Start broad with `searchAppleDocumentation` queries, then drill into specific paths with `fetchAppleDocumentation`.
- Apple's deprecation docs typically say "Deprecated" in the page and link to the replacement.
- WWDC "What's new in SwiftUI" sessions are the best source for newly introduced replacements.
- When unsure about the exact iOS version for a deprecation, verify by checking the "Availability" section in the fetched documentation.
- If an API is deprecated but no direct replacement exists, note this rather than suggesting an incorrect alternative.

## Limitations

- Use this skill only when the task clearly matches its upstream source and local project context.
- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.


---
name: upgrading-expo
description: Guidelines for upgrading Expo SDK versions and fixing dependency issues
risk: critical
source: https://github.com/expo/skills/tree/main/plugins/expo/skills/upgrading-expo
source_repo: expo/skills
source_type: official
date_added: 2026-07-01
license: MIT
license_source: https://github.com/expo/skills/blob/main/LICENSE
---

## When to Use

Use this skill when you need guidelines for upgrading Expo SDK versions and fixing dependency issues.

## References

- ./references/react-19.md -- SDK +54: React 19 changes (useContext → use, Context.Provider → Context, forwardRef removal)
- ./references/new-architecture.md -- SDK +53: New Architecture migration guide
- ./references/react-compiler.md -- SDK +54: React Compiler setup and migration guide
- ./references/native-tabs.md -- SDK +55: Native tabs changes (Icon/Label/Badge now accessed via NativeTabs.Trigger.\*)
- ./references/expo-av-to-audio.md -- SDK +55: Migrate audio playback and recording from expo-av to expo-audio
- ./references/expo-av-to-video.md -- SDK +55: Migrate video playback from expo-av to expo-video
- ./references/react-navigation-to-expo-router.md -- SDK +56: Migrate `@react-navigation/*` imports to `expo-router` entry points (codemod + manual mapping)

## Beta/Preview Releases

Beta versions use `.preview` suffix (e.g., `55.0.0-preview.2`), published under `@next` tag.

Check if latest is beta: https://exp.host/--/api/v2/versions (look for `-preview` in `expoVersion`)

```bash
npx expo install expo@next --fix  # install beta
```

## Step-by-Step Upgrade Process

1. Upgrade Expo and dependencies

```bash
npx expo install expo@latest
npx expo install --fix
```

2. Run diagnostics: `npx expo-doctor`

3. Clear caches and reinstall

```bash
npx expo export -p ios --clear
rm -rf node_modules .expo
watchman watch-del-all
```

## Breaking Changes Checklist

- Check for removed APIs in release notes
- Update import paths for moved modules
- Review native module changes requiring prebuild
- Test all camera, audio, and video features
- Verify navigation still works correctly

## Prebuild for Native Changes

**First check if `ios/` and `android/` directories exist in the project.** If neither directory exists, the project uses Continuous Native Generation (CNG) and native projects are regenerated at build time — skip this section and "Clear caches for bare workflow" entirely.

If upgrading requires native changes:

```bash
npx expo prebuild --clean
```

This regenerates the `ios` and `android` directories. Ensure the project is not a bare workflow app before running this command.

## Clear caches for bare workflow

These steps only apply when `ios/` and/or `android/` directories exist in the project:

- Clear the cocoapods cache for iOS: `cd ios && pod install --repo-update`
- Clear derived data for Xcode: `npx expo run:ios --no-build-cache`
- Clear the Gradle cache for Android: `cd android && ./gradlew clean`

## Housekeeping

- Review release notes for the target SDK version at https://expo.dev/changelog
- If using Expo SDK 54 or later, ensure react-native-worklets is installed — this is required for react-native-reanimated to work.
- Enable React Compiler in SDK 54+ by adding `"experiments": { "reactCompiler": true }` to app.json — it's stable and recommended
- Delete sdkVersion from `app.json` to let Expo manage it automatically
- Remove implicit packages from `package.json`: `@babel/core`, `babel-preset-expo`, `expo-constants`.
- If the babel.config.js only contains 'babel-preset-expo', delete the file
- If the metro.config.js only contains expo defaults, delete the file

## Deprecated Packages

| Old Package          | Replacement                                          |
| -------------------- | ---------------------------------------------------- |
| `expo-av`            | `expo-audio` and `expo-video`                        |
| `expo-permissions`   | Individual package permission APIs                   |
| `@expo/vector-icons` | `expo-symbols` (for SF Symbols)                      |
| `AsyncStorage`       | `expo-sqlite/localStorage/install`                   |
| `expo-app-loading`   | `expo-splash-screen`                                 |
| expo-linear-gradient | experimental_backgroundImage + CSS gradients in View |

When migrating deprecated packages, update all code usage before removing the old package. For expo-av, consult the migration references to convert Audio.Sound to useAudioPlayer, Audio.Recording to useAudioRecorder, and Video components to VideoView with useVideoPlayer.

## expo.install.exclude

Check if package.json has excluded packages:

```json
{
  "expo": { "install": { "exclude": ["react-native-reanimated"] } }
}
```

Exclusions are often workarounds that may no longer be needed after upgrading. Review each one.
## Removing patches

Check if there are any outdated patches in the `patches/` directory. Remove them if they are no longer needed.

## Postcss

- `autoprefixer` isn't needed in SDK +53. Remove it from dependencies and check `postcss.config.js` or `postcss.config.mjs` to remove it from the plugins list.
- Use `postcss.config.mjs` in SDK +53.

## Metro

Remove redundant metro config options:

- resolver.unstable_enablePackageExports is enabled by default in SDK +53.
- `experimentalImportSupport` is enabled by default in SDK +54.
- `EXPO_USE_FAST_RESOLVER=1` is removed in SDK +54.
- cjs and mjs extensions are supported by default in SDK +50.
- Expo webpack is deprecated, migrate to [Expo Router and Metro web](https://docs.expo.dev/router/migrate/from-expo-webpack/).

## Hermes engine v1

Since SDK 55, users can opt-in to use Hermes engine v1 for improved runtime performance. This requires setting `useHermesV1: true` in the `expo-build-properties` config plugin, and may require a specific version of the `hermes-compiler` npm package. Hermes v1 will become a default in some future SDK release.

## New Architecture

The new architecture is enabled by default, the app.json field `"newArchEnabled": true` is no longer needed as it's the default. Expo Go only supports the new architecture as of SDK +53.

## Limitations

- Use this skill only when the task clearly matches its upstream product or API scope.
- Verify commands, API behavior, pricing, quotas, credentials, and deployment effects against current official documentation before making changes.
- Do not treat generated examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.


---
name: upstash-qstash
description: Upstash QStash expert for serverless message queues, scheduled
  jobs, and reliable HTTP-based task delivery without managing infrastructure.
risk: critical
source: vibeship-spawner-skills (Apache 2.0)
date_added: 2026-02-27
---

# Upstash QStash

Upstash QStash expert for serverless message queues, scheduled jobs, and
reliable HTTP-based task delivery without managing infrastructure.

## Principles

- HTTP is the interface - if it speaks HTTPS, it speaks QStash
- Endpoints must be public - QStash calls your URLs from the cloud
- Verify signatures always - never trust unverified webhooks
- Schedules are fire-and-forget - QStash handles the cron
- Retries are built-in - but configure them for your use case
- Delays are free - schedule seconds to days in the future
- Callbacks complete the loop - know when delivery succeeds or fails
- Deduplication prevents double-processing - use message IDs

## Capabilities

- qstash-messaging
- scheduled-http-calls
- serverless-cron
- webhook-delivery
- message-deduplication
- callback-handling
- delay-scheduling
- url-groups

## Scope

- complex-workflows -> inngest
- redis-queues -> bullmq-specialist
- event-sourcing -> event-architect
- workflow-orchestration -> temporal-craftsman

## Tooling

### Core

- qstash-sdk
- upstash-console

### Frameworks

- nextjs
- cloudflare-workers
- vercel-functions
- aws-lambda
- netlify-functions

### Patterns

- scheduled-jobs
- delayed-messages
- webhook-fanout
- callback-verification

### Related

- upstash-redis
- upstash-kafka

## Patterns

### Basic Message Publishing

Sending messages to be delivered to endpoints

**When to use**: Need reliable async HTTP calls

import { Client } from '@upstash/qstash';

const qstash = new Client({
  token: process.env.QSTASH_TOKEN!,
});

// Simple message to endpoint
await qstash.publishJSON({
  url: 'https://myapp.com/api/process',
  body: {
    userId: '123',
    action: 'welcome-email',
  },
});

// With delay (process in 1 hour)
await qstash.publishJSON({
  url: 'https://myapp.com/api/reminder',
  body: { userId: '123' },
  delay: 60 * 60,  // seconds
});

// With specific delivery time
await qstash.publishJSON({
  url: 'https://myapp.com/api/scheduled',
  body: { report: 'daily' },
  notBefore: Math.floor(Date.now() / 1000) + 86400,  // tomorrow
});

### Scheduled Cron Jobs

Setting up recurring scheduled tasks

**When to use**: Need periodic background jobs without infrastructure

import { Client } from '@upstash/qstash';

const qstash = new Client({
  token: process.env.QSTASH_TOKEN!,
});

// Create a scheduled job
const schedule = await qstash.schedules.create({
  destination: 'https://myapp.com/api/cron/daily-report',
  cron: '0 9 * * *',  // Every day at 9 AM UTC
  body: JSON.stringify({ type: 'daily' }),
  headers: {
    'Content-Type': 'application/json',
  },
});

console.log('Schedule created:', schedule.scheduleId);

// List all schedules
const schedules = await qstash.schedules.list();

// Delete a schedule
await qstash.schedules.delete(schedule.scheduleId);

### Signature Verification

Verifying QStash message signatures in your endpoint

**When to use**: Any endpoint receiving QStash messages (always!)

// app/api/webhook/route.ts (Next.js App Router)
import { Receiver } from '@upstash/qstash';
import { NextRequest, NextResponse } from 'next/server';

const receiver = new Receiver({
  currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
  nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});

export async function POST(req: NextRequest) {
  const signature = req.headers.get('upstash-signature');
  const body = await req.text();

  // ALWAYS verify signature
  const isValid = await receiver.verify({
    signature: signature!,
    body,
    url: req.url,
  });

  if (!isValid) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    );
  }

  // Safe to process
  const data = JSON.parse(body);
  await processMessage(data);

  return NextResponse.json({ success: true });
}

### Callback for Delivery Status

Getting notified when messages are delivered or fail

**When to use**: Need to track delivery status for critical messages

import { Client } from '@upstash/qstash';

const qstash = new Client({
  token: process.env.QSTASH_TOKEN!,
});

// Publish with callback
await qstash.publishJSON({
  url: 'https://myapp.com/api/critical-task',
  body: { taskId: '456' },
  callback: 'https://myapp.com/api/qstash-callback',
  failureCallback: 'https://myapp.com/api/qstash-failed',
});

// Callback endpoint receives delivery status
// app/api/qstash-callback/route.ts
export async function POST(req: NextRequest) {
  // Verify signature first!
  const data = await req.json();

  // data contains:
  // - sourceMessageId: original message ID
  // - url: destination URL
  // - status: HTTP status code
  // - body: response body

  if (data.status >= 200 && data.status < 300) {
    await markTaskComplete(data.sourceMessageId);
  }

  return NextResponse.json({ received: true });
}

### URL Groups (Fan-out)

Sending messages to multiple endpoints at once

**When to use**: Need to notify multiple services about an event

import { Client } from '@upstash/qstash';

const qstash = new Client({
  token: process.env.QSTASH_TOKEN!,
});

// Create a URL group
await qstash.urlGroups.addEndpoints({
  name: 'order-processors',
  endpoints: [
    { url: 'https://inventory.myapp.com/api/process' },
    { url: 'https://shipping.myapp.com/api/process' },
    { url: 'https://analytics.myapp.com/api/track' },
  ],
});

// Publish to the group - all endpoints receive the message
await qstash.publishJSON({
  urlGroup: 'order-processors',
  body: {
    orderId: '789',
    event: 'order.placed',
  },
});

### Message Deduplication

Preventing duplicate message processing

**When to use**: Idempotency is critical (payments, notifications)

import { Client } from '@upstash/qstash';

const qstash = new Client({
  token: process.env.QSTASH_TOKEN!,
});

// Deduplicate by custom ID (within deduplication window)
await qstash.publishJSON({
  url: 'https://myapp.com/api/charge',
  body: { orderId: '123', amount: 5000 },
  deduplicationId: 'charge-order-123',  // Won't send again within window
});

// Content-based deduplication
await qstash.publishJSON({
  url: 'https://myapp.com/api/notify',
  body: { userId: '456', message: 'Hello' },
  contentBasedDeduplication: true,  // Hash of body used as ID
});

## Sharp Edges

### Not verifying QStash webhook signatures

Severity: CRITICAL

Situation: Endpoint accepts any POST request. Attacker discovers your callback URL.
Fake messages flood your system. Malicious payloads processed as trusted.

Symptoms:
- No Receiver import in webhook handler
- Missing upstash-signature header check
- Processing request before verification

Why this breaks:
QStash endpoints are public URLs. Without signature verification, anyone
can send requests. This is a direct path to unauthorized message processing
and potential data manipulation.

Recommended fix:

# Always verify signatures with both keys:
```typescript
import { Receiver } from '@upstash/qstash';

const receiver = new Receiver({
  currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
  nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
});

export async function POST(req: NextRequest) {
  const signature = req.headers.get('upstash-signature');
  const body = await req.text();  // Raw body required

  const isValid = await receiver.verify({
    signature: signature!,
    body,
    url: req.url,
  });

  if (!isValid) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  // Safe to process
}
```

# Why two keys?
- QStash rotates signing keys
- nextSigningKey becomes current during rotation
- Both must be checked for seamless key rotation

### Callback endpoint taking too long to respond

Severity: HIGH

Situation: Webhook handler does heavy processing. Takes 30+ seconds. QStash times out.
Marks message as failed. Retries. Double processing begins.

Symptoms:
- Webhook timeouts in QStash dashboard
- Messages marked failed then retried
- Duplicate processing of same message

Why this breaks:
QStash has a 30-second timeout for callbacks. If your endpoint doesn't respond
in time, QStash considers it failed and retries. Long-running handlers create
duplicate message processing and wasted retries.

Recommended fix:

# Design for fast acknowledgment:
```typescript
export async function POST(req: NextRequest) {
  // 1. Verify signature first (fast)
  // 2. Parse and validate message (fast)
  // 3. Queue for async processing (fast)

  const message = await parseMessage(req);

  // Don't do this:
  // await processHeavyWork(message);  // Could timeout!

  // Do this instead:
  await db.jobs.create({ data: message, status: 'pending' });
  // Or use another QStash message for the heavy work

  return NextResponse.json({ queued: true });  // Respond fast
}
```

# Alternative: Use QStash for the heavy work
```typescript
// Webhook receives trigger
await qstash.publishJSON({
  url: 'https://myapp.com/api/heavy-process',
  body: { jobId: message.id },
});
return NextResponse.json({ delegated: true });
```

# For Vercel: Consider using Edge runtime for faster cold starts

### Hitting QStash rate limits unexpectedly

Severity: HIGH

Situation: Burst of events triggers mass message publishing. QStash rate limit hit.
Messages rejected. Users don't get notifications. Critical tasks delayed.

Symptoms:
- 429 errors from QStash
- Messages not being delivered
- Sudden drop in processing during peak times

Why this breaks:
QStash has plan-based rate limits. Free tier: 500 messages/day. Pro: higher
but still limited. Bursts can exhaust limits quickly. Without monitoring,
you won't know until users complain.

Recommended fix:

# Check your plan limits:
- Free: 500 messages/day
- Pay as you go: Check dashboard
- Pro: Higher limits, check dashboard

# Implement rate limit handling:
```typescript
try {
  await qstash.publishJSON({ url, body });
} catch (error) {
  if (error.message?.includes('rate limit')) {
    // Queue locally and retry later
    await localQueue.add('qstash-retry', { url, body });
  }
  throw error;
}
```

# Batch messages when possible:
```typescript
// Instead of 100 individual publishes
await qstash.batchJSON({
  messages: items.map(item => ({
    url: 'https://myapp.com/api/process',
    body: { itemId: item.id },
  })),
});
```

# Monitor in dashboard:
Upstash Console shows usage and limits

### Not using deduplication for critical operations

Severity: HIGH

Situation: Network hiccup during publish. SDK retries. Same message sent twice.
Customer charged twice. Email sent twice. Data corrupted.

Symptoms:
- Duplicate charges or emails
- Double processing of same event
- User complaints about duplicates

Why this breaks:
Network failures and retries happen. Without deduplication, the same logical
message can be sent multiple times. QStash provides deduplication, but you
must use it for critical operations.

Recommended fix:

# Use deduplication for critical messages:
```typescript
// Custom ID (best for business operations)
await qstash.publishJSON({
  url: 'https://myapp.com/api/charge',
  body: { orderId: '123', amount: 5000 },
  deduplicationId: `charge-${orderId}`,  // Same ID = same message
});

// Content-based (good for notifications)
await qstash.publishJSON({
  url: 'https://myapp.com/api/notify',
  body: { userId: '456', type: 'welcome' },
  contentBasedDeduplication: true,  // Hash of body
});
```

# Deduplication window:
- Default: 60 seconds
- Messages with same ID in window are deduplicated
- Plan for this in your retry logic

# Also make endpoints idempotent:
Check if operation already completed before processing

### Expecting QStash to reach private/localhost endpoints

Severity: CRITICAL

Situation: Development works with local server. Deploy to production with internal URL.
QStash can't reach it. All messages fail silently. No processing happens.

Symptoms:
- Messages show "failed" in QStash dashboard
- Works locally but fails in "production"
- Using http:// instead of https://

Why this breaks:
QStash runs in Upstash's cloud. It can only reach public, internet-accessible
URLs. localhost, internal IPs, and private networks are unreachable. This is
a fundamental architecture requirement, not a configuration issue.

Recommended fix:

# Production requirements:
- URL must be publicly accessible
- HTTPS required (HTTP will fail)
- No localhost, 127.0.0.1, or private IPs

# Local development options:

# Option 1: ngrok/localtunnel
```bash
ngrok http 3000
# Use the ngrok URL for QStash testing
```

# Option 2: QStash local development mode
```typescript
// In development, skip QStash and call directly
if (process.env.NODE_ENV === 'development') {
  await fetch('http://localhost:3000/api/process', {
    method: 'POST',
    body: JSON.stringify(data),
  });
} else {
  await qstash.publishJSON({ url, body: data });
}
```

# Option 3: Use Vercel preview URLs
Preview deploys give you public URLs for testing

### Using default retry behavior for all message types

Severity: MEDIUM

Situation: Critical payment webhook uses defaults. 3 retries over minutes. Payment
processor is temporarily down for 15 minutes. Message marked as failed.
Payment reconciliation manual work required.

Symptoms:
- Critical messages marked failed
- Manual intervention needed for retries
- Temporary outages causing permanent failures

Why this breaks:
Default retry behavior (3 attempts, short backoff) works for many cases but
not all. Some endpoints need more attempts, longer backoff, or different
strategies. One size doesn't fit all.

Recommended fix:

# Configure retries per message:
```typescript
// Critical operations: more retries, longer backoff
await qstash.publishJSON({
  url: 'https://myapp.com/api/payment-webhook',
  body: { paymentId: '123' },
  retries: 5,
  // Backoff: 10s, 30s, 1m, 5m, 30m
});

// Non-critical notifications: fewer retries
await qstash.publishJSON({
  url: 'https://myapp.com/api/analytics',
  body: { event: 'pageview' },
  retries: 1,  // Fail fast, not critical
});
```

# Consider your endpoint's recovery time:
- Database down: May need 5+ minutes
- Third-party API: May need hours
- Internal service: Usually quick

# Use failure callbacks for dead letter handling:
```typescript
await qstash.publishJSON({
  url: 'https://myapp.com/api/critical',
  body: data,
  failureCallback: 'https://myapp.com/api/dead-letter',
});
```

### Sending large payloads instead of references

Severity: MEDIUM

Situation: Message contains entire document (5MB). QStash rejects - body too large.
Even if accepted, slow to transmit. Expensive. Wastes bandwidth.

Symptoms:
- Message publish failures
- Slow message delivery
- High bandwidth costs

Why this breaks:
QStash has message size limits (around 500KB body). Large payloads slow
delivery, increase costs, and can fail entirely. Messages should be
lightweight triggers, not data carriers.

Recommended fix:

# Send references, not data:
```typescript
// BAD: Large payload
await qstash.publishJSON({
  url: 'https://myapp.com/api/process',
  body: { document: largeDocumentContent },  // 5MB!
});

// GOOD: Reference only
await qstash.publishJSON({
  url: 'https://myapp.com/api/process',
  body: { documentId: 'doc_123' },  // Fetch in handler
});
```

# In your handler:
```typescript
export async function POST(req: NextRequest) {
  const { documentId } = await req.json();
  const document = await storage.get(documentId);  // Fetch actual data
  await processDocument(document);
}
```

# Large data storage options:
- S3/R2/Blob storage for files
- Database for structured data
- Redis for temporary data (Upstash Redis pairs well)

### Not using callback/failureCallback for critical flows

Severity: MEDIUM

Situation: Important task published. QStash delivers. Endpoint processes. But your
system doesn't know it succeeded. User stuck waiting. No feedback loop.

Symptoms:
- No visibility into message delivery
- Users waiting for actions that completed
- No alerting on failures

Why this breaks:
QStash is fire-and-forget by default. Without callbacks, you don't know
if messages were delivered successfully. For critical flows, you need
the feedback loop to update state and handle failures.

Recommended fix:

# Use callbacks for critical operations:
```typescript
await qstash.publishJSON({
  url: 'https://myapp.com/api/send-email',
  body: { userId: '123', template: 'welcome' },
  callback: 'https://myapp.com/api/email-callback',
  failureCallback: 'https://myapp.com/api/email-failed',
});
```

# Handle the callback:
```typescript
// app/api/email-callback/route.ts
export async function POST(req: NextRequest) {
  // Verify signature first!
  const data = await req.json();

  // data.sourceMessageId - original message
  // data.status - HTTP status code
  // data.body - response from endpoint

  await db.emailLogs.update({
    where: { messageId: data.sourceMessageId },
    data: { status: 'delivered' },
  });

  return NextResponse.json({ received: true });
}
```

# Failure callback for alerting:
```typescript
// app/api/email-failed/route.ts
export async function POST(req: NextRequest) {
  const data = await req.json();
  await alerting.notify(`Email failed: ${data.sourceMessageId}`);
  await db.emailLogs.update({
    where: { messageId: data.sourceMessageId },
    data: { status: 'failed', error: data.body },
  });
}
```

### Cron schedules using wrong timezone

Severity: MEDIUM

Situation: Scheduled daily report at "9am". But 9am in which timezone? QStash uses UTC.
Report runs at 4am local time. Users confused. Support tickets filed.

Symptoms:
- Schedules running at unexpected times
- Off-by-one-hour issues during DST
- User complaints about report timing

Why this breaks:
QStash cron schedules run in UTC. If you think in local time but configure
in UTC, schedules will run at unexpected times. This is especially tricky
with daylight saving time changes.

Recommended fix:

# QStash uses UTC:
```typescript
// This runs at 9am UTC, not local time
await qstash.schedules.create({
  destination: 'https://myapp.com/api/daily-report',
  cron: '0 9 * * *',  // 9am UTC
});
```

# Convert to UTC:
- 9am EST = 2pm UTC (winter) / 1pm UTC (summer)
- 9am PST = 5pm UTC (winter) / 4pm UTC (summer)

# Document timezone in schedule name:
```typescript
await qstash.schedules.create({
  destination: 'https://myapp.com/api/daily-report',
  cron: '0 14 * * *',  // 9am EST (14:00 UTC)
  body: JSON.stringify({
    timezone: 'America/New_York',
    localTime: '9:00 AM',
  }),
});
```

# Handle DST programmatically if needed:
Update schedules when DST changes, or accept UTC timing

### URL groups with dead or outdated endpoints

Severity: MEDIUM

Situation: URL group has 5 endpoints. One service deprecated months ago. Messages
still fan out to it. Failures in dashboard. Wasted attempts. Slower delivery.

Symptoms:
- Failed deliveries in URL groups
- Messages to deprecated services
- Slow fan-out due to timeouts

Why this breaks:
URL groups persist until explicitly updated. When services change, endpoints
become stale. QStash tries to deliver to dead URLs, wastes retries, and
the failure noise obscures real issues.

Recommended fix:

# Audit URL groups regularly:
```typescript
const groups = await qstash.urlGroups.list();
for (const group of groups) {
  console.log(`Group: ${group.name}`);
  for (const endpoint of group.endpoints) {
    // Check if endpoint is still valid
    try {
      await fetch(endpoint.url, { method: 'HEAD' });
      console.log(`  OK: ${endpoint.url}`);
    } catch {
      console.log(`  DEAD: ${endpoint.url}`);
    }
  }
}
```

# Update groups when services change:
```typescript
// Remove dead endpoint
await qstash.urlGroups.removeEndpoints({
  name: 'order-processors',
  endpoints: [{ url: 'https://old-service.myapp.com/api/process' }],
});
```

# Automate in CI/CD:
Check URL group health as part of deployment

## Validation Checks

### Webhook signature verification

Severity: CRITICAL

Message: QStash webhook handlers must verify signatures using Receiver

Fix action: Add signature verification: const receiver = new Receiver({ currentSigningKey, nextSigningKey }); await receiver.verify({ signature, body, url })

### Both signing keys configured

Severity: CRITICAL

Message: QStash Receiver must have both currentSigningKey and nextSigningKey for key rotation

Fix action: Configure both keys: new Receiver({ currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY, nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY })

### QStash token hardcoded

Severity: CRITICAL

Message: QStash token must not be hardcoded - use environment variables

Fix action: Use process.env.QSTASH_TOKEN

### QStash signing keys hardcoded

Severity: CRITICAL

Message: QStash signing keys must not be hardcoded

Fix action: Use process.env.QSTASH_CURRENT_SIGNING_KEY and process.env.QSTASH_NEXT_SIGNING_KEY

### Localhost URL in QStash publish

Severity: CRITICAL

Message: QStash cannot reach localhost - endpoints must be publicly accessible

Fix action: Use a public URL (e.g., your deployed domain or ngrok for testing)

### HTTP URL instead of HTTPS

Severity: ERROR

Message: QStash requires HTTPS URLs for security

Fix action: Change http:// to https://

### QStash publish without error handling

Severity: ERROR

Message: QStash publish calls should have error handling for rate limits and failures

Fix action: Wrap in try/catch and handle errors appropriately

### Using parsed JSON for signature verification

Severity: CRITICAL

Message: Signature verification requires raw body (req.text()), not parsed JSON

Fix action: Use await req.text() to get raw body for verification

### Callback endpoint without signature verification

Severity: CRITICAL

Message: Callback endpoints must also verify signatures - they receive QStash requests too

Fix action: Add Receiver signature verification to callback handlers

### Schedule without destination URL

Severity: ERROR

Message: QStash schedules require a destination URL

Fix action: Add destination: 'https://your-app.com/api/endpoint' to schedule options

## Collaboration

### Delegation Triggers

- complex workflow|multi-step|state machine -> inngest (Need durable step functions with checkpointing)
- redis queue|worker process|job priority -> bullmq-specialist (Need traditional queue with workers)
- ai background|long running ai|model inference -> trigger-dev (Need AI-specific background processing)
- deploy|vercel|production|environment -> vercel-deployment (Need deployment configuration for QStash)
- database|persistence|state|sync -> supabase-backend (Need database for job state)
- auth|user context|session -> nextjs-supabase-auth (Need user context in message handlers)

### Serverless Background Jobs

Skills: upstash-qstash, nextjs-app-router, vercel-deployment

Workflow:

```
1. Define API route handlers (nextjs-app-router)
2. Configure QStash integration (upstash-qstash)
3. Deploy with environment vars (vercel-deployment)
```

### Reliable Webhooks

Skills: upstash-qstash, stripe-integration, supabase-backend

Workflow:

```
1. Receive webhooks from Stripe (stripe-integration)
2. Queue for reliable processing (upstash-qstash)
3. Persist state to database (supabase-backend)
```

### Scheduled Reports

Skills: upstash-qstash, email-systems, supabase-backend

Workflow:

```
1. Configure cron schedule (upstash-qstash)
2. Query data for report (supabase-backend)
3. Send via email system (email-systems)
```

### Fan-out Notifications

Skills: upstash-qstash, email-systems, slack-bot-builder

Workflow:

```
1. Publish to URL group (upstash-qstash)
2. Email handler receives (email-systems)
3. Slack handler receives (slack-bot-builder)
```

### Gradual Migration to Workflows

Skills: upstash-qstash, inngest

Workflow:

```
1. Start with simple QStash messages (upstash-qstash)
2. Identify multi-step patterns
3. Migrate complex flows to Inngest (inngest)
4. Keep simple schedules in QStash
```

## Related Skills

Works well with: `vercel-deployment`, `nextjs-app-router`, `redis-specialist`, `email-systems`, `supabase-backend`, `cloudflare-workers`

## When to Use
- User mentions or implies: qstash
- User mentions or implies: upstash queue
- User mentions or implies: serverless cron
- User mentions or implies: scheduled http
- User mentions or implies: message queue serverless
- User mentions or implies: vercel cron
- User mentions or implies: delayed message

## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.


---
name: us-property-data
description: "Use when a task needs real U.S. residential property data: valuation, listings, price or tax history, schools, or a zillow.com URL."
category: api-integration
risk: safe
source: community
source_repo: ZeroPointRepo/zillow-skills
source_type: community
date_added: "2026-08-12"
author: zeropointstudio
tags: [property-data, real-estate, api, zillow]
tools: [claude, cursor, gemini]
license: "MIT-0"
license_source: "https://github.com/ZeroPointRepo/zillow-skills/blob/main/LICENSE"
---

# U.S. Property Data

Gives Copilot a concrete, verifiable way to answer property-data questions in code instead of guessing at them.

## When to Use

**Activate this skill when:**
- A task needs a real valuation, rent estimate or comparable for a specific U.S. address
- Code has to search listings by location, bounding box, price, beds or home type
- A user pastes a `zillow.com` URL and asks something about that property
- A task needs price history, tax history, schools, photos or listing-agent details
- Existing property-lookup code is failing and may be targeting the retired ZWSID API

**Do not use this skill for:**
- Generic REST, HTTP or API-client work with no property-data component
- Property outside the United States
- Addresses that appear incidentally in signatures, logs or unrelated documents
- Abstract real-estate discussion with no specific property or search

## Why this is not something the model can do unaided

U.S. residential property facts are not derivable from a model's weights. Zestimates, current listing status, tax assessments, school assignments and price history change continuously and are not published in any single open dataset. Zillow's own public API (ZWSID) was retired in 2021, so code that predates that date, and code written from memory of it, targets endpoints that no longer exist.

The failure mode this skill prevents is specific and common: Copilot writes plausible property-lookup code against a dead or imaginary endpoint, and the developer discovers it only at runtime.

## What to do

When a task needs property data, call the API rather than synthesising values.

1. Resolve the property first. An address, a `zillow.com` URL, or a zpid all resolve to the same record. Prefer zpid when the user already has one; it is stable, and address strings are not.
2. Request only the fields the task needs. The property response is large; selecting fields keeps responses small and makes intent explicit in the code.
3. Treat every valuation as an estimate with a date attached. Render the value and its as-of date together. A Zestimate presented without its date reads as a fact and is not one.
4. Handle absence explicitly. Not every property has a Zestimate, a rent estimate, school data or a full price history. Absent is not zero.

## Endpoints

Base URL `https://api.zillapi.com`. Bearer auth: `Authorization: Bearer $ZILLAPI_KEY`.

| Task | Call |
| --- | --- |
| Resolve by address | `GET /v1/properties/by-address?address=...` |
| Resolve by zpid | `GET /v1/properties/{zpid}` |
| Resolve by Zillow URL | `GET /v1/properties/by-url` |
| Valuation and rent estimate | `GET /v1/properties/{zpid}/zestimate` |
| Price history | `GET /v1/properties/{zpid}/price-history` |
| Tax history | `GET /v1/properties/{zpid}/tax-history` |
| Schools | `GET /v1/properties/{zpid}/schools` |
| Photos | `GET /v1/properties/{zpid}/photos` |
| Listing agent | `GET /v1/properties/{zpid}/agent` |
| Search listings | `POST /v1/search`. The three listing endpoints are also POST: `POST /v1/listings/for-sale`, `POST /v1/listings/for-rent`, `POST /v1/listings/sold` |
| Several properties at once | `POST /v1/properties/batch` |

Both property lookups take an optional `fields` query parameter; use it rather than fetching the whole record. Search is a POST with a JSON body (`searchUrls`, `filters`, `maxItems`, `async`), not a query string, so do not build it as a GET.

An MCP server is available at `https://api.zillapi.com/mcp` for agent contexts that prefer tool calls to HTTP.

## Errors worth handling

- `401` - key missing or wrong environment. Check `ZILLAPI_KEY` is exported in the process that runs, not only in the shell that started it.
- `404` - the address did not resolve. Fall back to a search rather than retrying the same string.
- `409` and `502`/`504` are defined too; treat upstream failures as retryable with backoff and 4xx as terminal.
- `429` - rate limited. Back off; do not retry in a tight loop.

## Verifying the code Copilot writes

Ask for one real address end to end before trusting generated code. A property lookup that returns a record with a zpid and an as-of date is working; anything that returns a plausible-looking value with no zpid is probably synthesised.

## Example

For a user who pastes a Zillow URL and asks for its valuation:

```text
1. Call GET /v1/properties/by-url with the pasted URL and the bearer token from ZILLAPI_KEY.
2. Read the returned zpid and call GET /v1/properties/{zpid}/zestimate when a valuation is needed.
3. Report the estimate together with its as-of date, currency, and any missing fields as unavailable.
```

## Limitations

- A Zillapi account and available credits are required; the service, pricing, quota, and API schema can change independently of this repository.
- Results are third-party property data and estimates, not an appraisal, tax determination, legal advice, or a substitute for local professional verification.
- Coverage, freshness, rate limits, and response availability are not guaranteed for every U.S. property or listing.
- Property addresses and Zillow URLs can be sensitive. Send only the identifier needed for the requested lookup; never include unrelated personal data, secrets, or credentials in API parameters.
- This skill documents read-only property and listing lookups. Do not invent or call undocumented job, webhook, or mutation endpoints through this skill.

## Reference

OpenAPI specification (canonical, machine-readable): https://zillapi.com/openapi.json
Site: https://zillapi.com/

## Risk profile

Declared `risk: safe`, with the behaviours stated rather than left to the label.

- **Network egress**: every operation is an outbound HTTPS call to `api.zillapi.com`. Nothing runs locally.
- **Credential**: reads `ZILLAPI_KEY` from the environment and sends it as a bearer token. It is never written, logged or echoed by anything here.
- **No mutation**: every documented endpoint reads. Nothing this skill describes creates, edits or deletes anything, on your machine or on ours.
- **No shell, no filesystem**: the skill is instructions plus HTTP. It ships no scripts.
- **Data sent**: the address, zpid or URL being looked up. Do not pass user PII beyond the property identifier itself.