Installs into .claude/skills of the current project.
Are you the author of Papyrus Reference?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/shugokifable-papyrus-reference)
---
name: papyrus-reference
description: Use when reading or writing Papyrus and an exact function/event signature, parameter type, return type, flag, or API semantic must be verified.
---
# Papyrus Reference
## Overview
This skill provides offline lookup for Papyrus function signatures, parameter shapes, return types, flags, and doc-comments. The reference corpus is generated from BellCube's [papyrus-index](https://github.com/BellCubeDev/papyrus-index) and ships in this skill's `references/` tree. The body walks the lookup procedure, the surgical-read pattern that uses each index entry's `line_start`/`line_end` to read only the relevant entry block from its source file, the bundled-or-warn fallback when a function isn't in the corpus, and the tier strategy that gates SKSE-plugin sources by modlist content.
This skill covers the **API surface** — function signatures and docs. Reading actual `.psc` source from a modlist, and compiling `.psc` → `.pex`, are separate concerns handled with your normal file-reading and compiler tooling.
For the compiles-clean-but-misbehaves traps a correct signature does NOT reveal — `GetForm` returning `None` for the whole ESL range, `SendModEvent`'s 4-arg handler arity, `FormList.HasForm` missing base-`NPC_` entries, mixed storage backends, `Utility.Wait` in a paused menu, literal/docstring escaping, common-noun type collisions — see [references/silent-biters.md](references/silent-biters.md). It is a hand-curated companion, separate from the by-construction signature corpus.
## First step
When you encounter a Papyrus function call you need to verify, or when authoring a `.psc`, open the function index at `references/index.jsonl`. The index is JSONL — one entry per line — and resolves an unqualified function name to the per-script reference file that documents it AND the 1-indexed `line_start`/`line_end` range of the entry block within that file. The index is grep-friendly: the entries are compact JSON (no spaces after colons), so match the **full quoted token** `"name":"FunctionName"` — a spaced pattern like `"name": "…"` matches nothing.
Once you have the matching index entry, do **NOT** read the whole reference file — use the entry's `line_start`/`line_end` to read just the entry block via your file-reading tool's line-range (offset/limit) capability. The whole-file load is typically 10-300 KB; the targeted block read is 200-1500 bytes. Don't bulk-load the index either; it is ~1.5 MB and that defeats the per-session token economics. Use targeted reads throughout.
## Reasoning framework / Lookup procedure
1. **Identify the unqualified function name** from the call site — for `Self.GetActorValue("Health")` the name is `GetActorValue`; for `StringUtil.Substring(str, 0, 4)` the name is `Substring`.
2. **Look up the name in `references/index.jsonl`.** Match the **full quoted token** — `"name":"<FunctionName>"`, closing quote included — so the hit is exact and field-scoped. The index is **compact JSON** (no spaces after colons), so a spaced pattern like `"name": "…"`, or one missing the leading quote like `name:"…"`, matches **zero lines** even for a function that is present — a false "absent" indistinguishable from a true miss. If a lookup you expect to hit returns nothing, validate the pattern against a guaranteed-present token first (`"name":"OnInit"` resolves to several entries); only trust a zero-result as "not in the corpus" once the method itself is proven. There are three result shapes:
- **Single match.** Pull the entry's `file`, `line_start`, and `line_end`. Confirm signature (return type, parameter types, default values), flags (`Native` / `Global` / `Hidden` / `BetaOnly` / `DebugOnly`), and doc-comment if present via the block read in step 3.
- **Multiple matches across sources.** Use the source qualifier from the call site to disambiguate. For unqualified calls inside a script body, disambiguation hinges on the calling script's `extends` chain — `Self.GetActorValue(...)` inside a script extending `Actor` resolves to `Actor#GetActorValue`. For global calls, the `Script.Function` qualifier in the source is authoritative: `StringUtil.Substring` resolves to the entry whose `qualified` field is `StringUtil.Substring`.
- **No match in the bundled index.** Proceed to the "Bundled-or-warn" section below.
3. **Block-read the resolved reference entry.** Read only lines `line_start`..`line_end` from the entry's `file` (use your file tool's offset/limit). This returns just the entry's lines — typically 8-30 lines for a function block. Do **NOT** read the whole reference file; a whole-file read on `Actor.md` (303 functions, ~80 KB) or `papyrusutil.md` (~140 KB multi-script source) costs 25-50× more tokens than the block read needs. Confirm parameters in order, types, default values, return type, flags. About 51% of functions carry doc-comments; the other 49% surface signature-only entries — that's normal, not a sign of missing data.
4. **For event lookups, property lookups, and struct member lookups,** the same index handles them — the `kind` field discriminates (`global` / `instance-method` / `event` / `property`). The per-script reference file groups them under `## Events`, `## Properties`, and `## Structs` sections.
## Bundled-or-warn — never invent a signature
When the bundled index has no match for the requested function:
1. **Emit an explicit warning to the user.** Use this shape:
```
No Papyrus reference for `FunctionName` — not present in the bundled corpus.
I will not invent a signature.
Options to proceed:
- Verify the function name spelling (typos are the #1 cause of bundled-miss)
- Check whether the function lives in an SKSE plugin not in the bundled set
(~45 popular plugins are covered; brand-new or niche plugins may not be)
- Investigate via Creation Kit / Papyrus modding forums / the mod's shipped
source if it includes .psc files
- Author a custom reference file following this skill's index + block shape
- Skip the function call if it's not load-bearing for the current task
```
2. **Never invent a signature.** A confidently-asserted but wrong signature can cause the user to ship code that compiles but misbehaves at runtime, or to file a compile failure as an upstream bug when it's actually an authoring error — both are recoverable only through painful debugging the user shouldn't have been put through. A clear non-answer beats a confident wrong one.
## Function index
The index lives at `references/index.jsonl`. Entries are **compact JSON** — one per line, no spaces after colons (match with a full quoted token like `"name":"GetActorValue"`, never a spaced `"name": "…"`). Per-entry shape:
```json
{"name":"GetActorValue","qualified":"Actor#GetActorValue","source":"vanilla","file":"references/vanilla/Actor.md","kind":"instance-method","line_start":721,"line_end":732}
{"name":"Substring","qualified":"StringUtil.Substring","source":"skse","file":"references/skse/StringUtil.md","kind":"global","line_start":131,"line_end":145}
{"name":"PushString","qualified":"PapyrusUtil.PushString","source":"papyrusutil","file":"references/papyrusutil.md","kind":"global","requires_plugin":"PapyrusUtilSE.dll","line_start":2610,"line_end":2622}
```
Fields:
- `name` — unqualified function/event/property name. This is the primary lookup key.
- `qualified` — `Script.Function` for global functions, `Script#Method` for instance methods, `Script.Event` for events, `Script.Property` for properties. Use this to disambiguate when `name` collides across sources.
- `source` — BellCube source directory (`vanilla`, `skse`, `papyrusutil`, `jcontainers`, etc.).
- `file` — relative path to the per-script reference markdown, rooted at the skill folder.
- `kind` — one of `global`, `instance-method`, `event`, `property`. Property entries appear in `## Properties` sections; events in `## Events`; functions split by `isGlobal` flag into `## Global Functions` vs `## Functions`.
- `line_start` / `line_end` — 1-indexed inclusive line range of the entry's block within `file`. Use these for the targeted block read. Typical block sizes: properties 6-10 lines, events 5-15 lines, functions 8-30 lines.
- `requires_plugin` (Tier 2 only) — the SKSE plugin DLL name that gates this entry's availability. Omitted for Tier 1 vanilla + skse entries (always available).
## Tier strategy
The corpus ships in three tiers. All bundled entries are present in `references/index.jsonl` regardless of tier; tier filtering happens at lookup time, not at install time.
- **Tier 1 — always available.** `vanilla/` (~25-30 scripts: `Actor`, `Form`, `ObjectReference`, `Quest`, `Game`, `Utility`, `Debug`, etc.) + `skse/` (~15-20 scripts: `StringUtil`, `Math`, `Input`, SKSE-additions to base types). These ship with any Skyrim install + SKSE, so there's no useful gating. Their index entries omit `requires_plugin`.
- **Tier 2 — modlist-gated.** ~45 popular SKSE plugin sources (`PapyrusUtil`, `JContainers`, `RaceMenu`, `MCMHelper`, etc.). Each Tier 2 entry's index row carries `requires_plugin` — the SKSE plugin DLL filename.
When you look up a function and the matching index entries are all Tier 2, confirm the gating plugin is present in the user's **active load order** before recommending the function — read the load order / active-plugins list if you have filesystem access to it, otherwise ask the user. If the plugin isn't active, fall through to the "Bundled-or-warn" path: the function exists in the bundle, but the user's modlist doesn't include the plugin that provides it.
- **Tier 3 — omitted.** Fallout 4 and Starfield sources are not included in the Skyrim corpus.
## Common mistakes
- **Inventing a function signature when the bundled index has no match.** Surface the explicit warning instead. If the user pushes back ("just guess"), explain that a wrong signature can cause silent runtime misbehavior or false-positive bug reports, and ask them to confirm via CK or source before proceeding.
- **Writing the index grep with spaces after colons, or dropping the leading quote.** Entries are compact JSON — `"name":"Dispel"`, never `"name": "Dispel"`. A spaced pattern (`"name": "`) or a quote-dropped one (`name:"`) matches **zero lines** for functions that are present, and that format-induced zero-match is indistinguishable from a true "not in corpus" — so it silently routes a present function into the bundled-or-warn path, the exact failure this skill exists to prevent. Match the full compact token (`"name":"Dispel"`); when an expected hit returns empty, suspect the pattern before the corpus.
- **Looking up by qualified name when only unqualified is provided.** The index's `name` field is unqualified — `Substring`, not `StringUtil.Substring`. Use `name` for the primary lookup and `qualified` only for disambiguation when multiple entries share a `name`.
- **Treating a missing doc-comment as a missing function.** About 49% of the corpus carries signature-only entries (no doc-block in the source `.psc`). The function exists and the signature is authoritative — there just isn't a doc-comment. Don't fall through to "Bundled-or-warn" on missing docs alone.
- **Loading a per-source reference file when the index pointed somewhere else.** Always trust the index's `file` field. If the user says "look up `Foo` in `Actor.md`" but the index resolves `Foo` to `references/skse/Form.md`, the index is authoritative — investigate the discrepancy before guessing.
- **Reading the whole reference file instead of block-reading via `line_start`/`line_end`.** Every index entry carries an inclusive 1-indexed line range pointing at the entry's block. A whole-file load on `Actor.md` is ~80 KB and pulls in 300+ unrelated entries — pure token waste. Only fall back to a whole-file read if the entry block is malformed (extremely rare; would indicate a corpus-generation bug worth reporting) or if you genuinely need cross-entry context like the script's `Extends` header.
- **Skipping the Tier 2 plugin-active check for a script-context lookup.** A `PapyrusUtil.PushString` call only makes sense if `PapyrusUtilSE.dll` is active. If the user's modlist doesn't include it, the call won't resolve at runtime — the bundled-or-warn path is the correct response, not "here's the signature, use it."
- **Conflating events with functions.** Events have `kind: "event"` in the index and live in `## Events` sections. Their signatures (`Event OnInit()`, `Event OnEffectStart(Actor akTarget, Actor akCaster)`) look like function signatures but they're invoked by the engine, not called. If a user asks "how do I call `OnEffectStart`," that's a confused question — surface that events are engine-driven, not user-called.
## Notes
- **Corpus provenance** — the `references/` tree is generated from BellCube's [papyrus-index](https://github.com/BellCubeDev/papyrus-index). To update coverage (e.g. a newly-released plugin), regenerate from upstream and refresh `references/` + `index.jsonl`.
- **Hand-curated companion** — `references/silent-biters.md` is NOT part of the BellCube-generated corpus (it carries semantic gotchas, not signatures) and is never resolved by the index lookup. Preserve it across any regeneration of `references/` + `index.jsonl`; it carries a hand-maintained-staleness duty.
- **Authoring custom reference files** — for plugins not in the bundled corpus, you can hand-author a per-script reference file plus matching `index.jsonl` entries following the same `name` / `qualified` / `source` / `file` / `kind` / `line_start` / `line_end` shape this skill uses. The lookup procedure above then resolves them identically.