Turn Standard Definitions into Queryable Data
You build a feature that needs skill matrices or role definitions.
The standard data sits in YAML files under
data/pathway/. If you parse the files yourself, you
reimplement modifier resolution, proficiency clamping, tier
classification, and validation. You also keep the code in sync as
the standard evolves.
@forwardimpact/libskill handles that derivation. Load
the standard data once with @forwardimpact/map. Then
call pure functions that return skill matrices, behaviour profiles,
responsibilities, and agent configurations. The library applies the
same fit-pathway rules, so your feature matches the
CLI.
Prerequisites
- Node.js 22+
- Install both packages:
npm install @forwardimpact/libskill @forwardimpact/map
-
Initialize the standard data at
data/pathway/. If you still need to do this, runnpx fit-pathway initand follow the prompts.
Load the standard data
@forwardimpact/map provides a DataLoader.
The loader reads every YAML file in the data directory. It returns
one object with all entities resolved:
import { createDataLoader } from "@forwardimpact/map/loader";
import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
const loader = createDataLoader(createDefaultRuntime());
const data = await loader.loadAllData("data/pathway");
createDataLoader takes a runtime, which holds the
injected collaborators for file reads.
createDefaultRuntime() from
@forwardimpact/libutil wires the real filesystem. Pass
your own runtime for other data sources.
The returned data object contains arrays for
disciplines, levels, tracks,
skills, behaviours,
capabilities, and drivers. Every
@forwardimpact/libskill function accepts these arrays.
The library never reads the filesystem. You control the data source.
The functions stay pure.
Derive a skill matrix
A skill matrix shows every skill relevant to a discipline at a
specific level, with the proficiency that level requires. Call
deriveSkillMatrix with a discipline, level, and
optionally a track:
import { deriveSkillMatrix } from "@forwardimpact/libskill";
const discipline = data.disciplines.find((d) => d.id === "software-engineering");
const level = data.levels.find((l) => l.id === "J070");
const matrix = deriveSkillMatrix({
discipline,
level,
skills: data.skills,
capabilities: data.capabilities,
});
console.log(JSON.stringify(matrix[0], null, 2));
Expected output (one entry):
{
"skillId": "architecture-design",
"skillName": "Architecture Design",
"capability": "design",
"capabilityRank": 1,
"isHumanOnly": false,
"type": "core",
"proficiency": "practitioner",
"proficiencyDescription": "You lead architecture for a product or platform area..."
}
Each entry in the matrix includes:
| Field | Meaning |
|---|---|
skillId |
unique identifier that matches the YAML source |
type |
core, supporting,
broad, or track
|
proficiency |
derived proficiency after the modifiers apply |
proficiencyDescription |
human-readable description of that proficiency |
isHumanOnly |
true for skills irrelevant to agents |
deriveSkillMatrix sorts the matrix by type. The order
is core, supporting, broad, then track. It sorts alphabetically
within each type.
Apply track specializations
Tracks adjust skill proficiencies and add track-specific skills through modifiers. Pass a track to see the difference:
const track = data.tracks.find((t) => t.id === "platform");
const generalMatrix = deriveSkillMatrix({
discipline,
level,
skills: data.skills,
capabilities: data.capabilities,
});
const platformMatrix = deriveSkillMatrix({
discipline,
level,
track,
skills: data.skills,
capabilities: data.capabilities,
});
console.log("General skills:", generalMatrix.length);
console.log("Platform skills:", platformMatrix.length);
Expected output:
General skills: 12
Platform skills: 16
The platform track adds skills such as Change Management, Incident
Management, Observability, and Performance Optimization. Those
skills do not appear in the generalist matrix. Skills already
present can also shift proficiency. A track modifier of
+1 on a capability raises every skill in that
capability by one proficiency level. The level's maximum clamps
the result.
Derive a behaviour profile
Behaviours describe how engineers approach their work. The behaviour profile shows the expected maturity for each behaviour at a given level:
import { deriveBehaviourProfile } from "@forwardimpact/libskill";
const profile = deriveBehaviourProfile({
discipline,
level,
behaviours: data.behaviours,
});
console.log(JSON.stringify(profile[0], null, 2));
Expected output (one entry):
{
"behaviourId": "systems-thinking",
"behaviourName": "Think in Systems",
"maturity": "role-modeling",
"maturityDescription": "You shape how teams approach problems..."
}
Track and discipline modifiers both affect behaviour maturity. A
discipline with
behaviourModifiers: { collaboration: 1 } raises the
collaboration maturity by one level from the base. A track with its
own modifier stacks on top.
Derive a complete role definition
Use deriveJob when you need the full picture. It covers
the skill matrix, the behaviour profile, the responsibilities, and
the expectations. deriveJob validates the combination
first. It returns null for an invalid pairing. One
example is a call without a track for a discipline that requires
one:
import { deriveJob } from "@forwardimpact/libskill";
const result = deriveJob({
discipline,
level,
track,
skills: data.skills,
behaviours: data.behaviours,
capabilities: data.capabilities,
});
if (!result) {
console.error("Invalid combination");
process.exit(1);
}
console.log(result.title);
console.log("Skills:", result.skillMatrix.length);
console.log("Behaviours:", result.behaviourProfile.length);
console.log("Responsibilities:", result.derivedResponsibilities.length);
Expected output:
Senior Engineer Software Engineer - Platform Engineering
Skills: 16
Behaviours: 5
Responsibilities: 4
The returned object contains id, title,
skillMatrix (same shape as
deriveSkillMatrix output),
behaviourProfile (same shape as
deriveBehaviourProfile),
derivedResponsibilities, and
expectations (scope, autonomy, influence, complexity).
Generate all valid roles
Use generateAllJobs to list every valid
discipline-level-track combination:
import { generateAllJobs } from "@forwardimpact/libskill";
const allJobs = generateAllJobs({
disciplines: data.disciplines,
levels: data.levels,
tracks: data.tracks,
skills: data.skills,
behaviours: data.behaviours,
});
console.log("Total valid roles:", allJobs.length);
console.log(
"Titles:",
allJobs.slice(0, 3).map((j) => j.title)
);
Expected output (values depend on your standard):
Total valid roles: 48
Titles: [
"Associate Engineer Clinical Informatics",
"Associate Engineer Data Engineer",
"Associate Engineer Software Engineer"
]
The function skips invalid combinations automatically. Each entry is
a full role definition (same shape as
deriveJob output).
Prepare display-ready views
When you need data shaped for a UI or report rather than raw
derivation output, use the view preparation functions.
prepareJobDetail adds driver coverage analysis and a
de-duplicated toolkit on top of the base derivation:
import { prepareJobDetail } from "@forwardimpact/libskill";
const view = prepareJobDetail({
discipline,
level,
track,
skills: data.skills,
behaviours: data.behaviours,
drivers: data.drivers,
capabilities: data.capabilities,
});
console.log(view.title);
console.log("Driver coverage:");
for (const d of view.driverCoverage) {
console.log(` ${d.name}: ${(d.coverage * 100).toFixed(0)}%`);
}
Expected output:
Senior Engineer Software Engineer - Platform Engineering
Driver coverage:
Velocity: 85%
Stability: 70%
For list views, prepareJobSummary returns only the
title, the counts, and the identifiers. It returns no full matrices.
Derive agent profiles
Agent profiles follow the same derivation path as role definitions. They also apply extra policies. The derivation excludes human-only skills. It keeps only the highest-level skills. It sorts skills and behaviours by level descending.
Use prepareAgentProfile when you need the filtered,
agent-optimized view:
import { prepareAgentProfile } from "@forwardimpact/libskill/profile";
const agentProfile = prepareAgentProfile({
discipline,
track,
level,
skills: data.skills,
behaviours: data.behaviours,
capabilities: data.capabilities,
});
console.log("Agent skills:", agentProfile.skillMatrix.length);
console.log("First skill:", agentProfile.skillMatrix[0].skillName);
The agent skill matrix is smaller than the full role matrix, because
the derivation removes human-only skills and collapses lower-level
duplicates. prepareAgentProfile sorts behaviours
strongest-first. That order helps agent instructions, where the most
important working styles should lead. For the full agent generation
pipeline (identity text, working styles, skill markdown), see
generateAgentProfile on the
@forwardimpact/libskill/agent subpath.
Subpath imports
The root import provides the most commonly used functions. For focused use, import from subpaths to load only what you need:
| Subpath | Key exports |
|---|---|
@forwardimpact/libskill |
deriveSkillMatrix,
deriveBehaviourProfile, deriveJob
|
@forwardimpact/libskill/matching |
calculateJobMatch, findMatchingJobs
|
@forwardimpact/libskill/progression |
analyzeProgression,
analyzeLevelProgression
|
@forwardimpact/libskill/agent |
generateAgentProfile,
deriveAgentSkills
|
@forwardimpact/libskill/profile |
prepareBaseProfile,
prepareAgentProfile
|
@forwardimpact/libskill/interview |
deriveInterviewQuestions |
@forwardimpact/libskill/job |
prepareJobDetail, prepareJobSummary
|
@forwardimpact/libskill/job-cache |
createJobCache, buildJobKey |
@forwardimpact/libskill/policies |
policies for filters, sorts, and predicates |
Cache derived roles
When you derive the same combination repeatedly, pass a cache to avoid redundant computation. One example is a loop that compares roles:
import { createJobCache } from "@forwardimpact/libskill/job-cache";
import { prepareJobDetail } from "@forwardimpact/libskill";
const cache = createJobCache();
const view = prepareJobDetail({
discipline, level, track,
skills: data.skills, behaviours: data.behaviours,
drivers: data.drivers, capabilities: data.capabilities,
jobCache: cache,
});
The cache keys on discipline ID, level ID, and track ID. Create one cache per request or operation. Do not share caches across data reloads.
Validate combinations before you derive
Not every discipline-level-track triple is valid. Some disciplines require a track. Some tracks have a minimum level. Check before you derive:
import { isValidJobCombination } from "@forwardimpact/libskill";
const valid = isValidJobCombination({ discipline, level, track: null });
console.log("Valid without track:", valid);
If the discipline has
validTracks: [null, "platform"], a call
without a track is valid. If it has
validTracks: ["platform", "sre"]
(no null), the discipline requires a track. The call
then returns false.
deriveJob calls this validation internally. It returns
null for invalid combinations. Use
isValidJobCombination to check validity without the
full derivation. One example is to disable invalid options in a
form.
Verify
You reach the outcome of this guide when:
-
createDataLoader(createDefaultRuntime()).loadAllData()gives you the standard data that@forwardimpact/libskillfunctions accept. - You derive a skill matrix for a discipline + level (+ optional track) and inspect each entry's type, proficiency, and description.
- You derive a behaviour profile and read each behaviour's maturity level.
-
You generate a complete role definition with
deriveJoband access its skill matrix, behaviour profile, responsibilities, and expectations. - You know how track modifiers shift proficiencies and maturities.