Everything you need to write a great procedure
OpenPSL turns the operational knowledge in your head into structured, shareable, exportable documents. This guide walks you through every feature — without jargon, with concrete examples.
Welcome
What is OpenPSL?
OpenPSL is an open standard — and a reference platform — for writing down how operational work actually gets done. Any procedure, any industry, any language. The standard defines a tiny universal schema; the platform gives you a calm visual editor to fill it in.
The thing you create is called a Procedure Pack. It captures the title, the goal, who does it, what you need, the steps, decisions, exceptions, risks, controls, and outputs. You don't have to fill in every field. You can start with what you know and refine over time.
Who is it for?
OpenPSL is for anyone whose work has steps — and that's almost everyone. The standard is intentionally domain-neutral. It doesn't care if your procedure runs in a hospital, on a farm, in a factory, in a courtroom, in a server room, or on a kitchen counter. If you can describe what gets done and in what order, you can write it as a Procedure Pack.
It's especially useful when:
- People need to follow the same flow across teams, shifts, geographies, or generations.
- Knowledge would otherwise leave with a person — a retirement, a resignation, a sabbatical, a sale of the business.
- Compliance or audit requires evidence of what the procedure is and how it has changed.
- You're training new people and want them to read, run, and improve the work from day one.
- You're building an AI agent that needs structured ground truth instead of free-form prompts.
- You want to preserve a craft — formal trade or informal practice — so it survives the people who carry it today.
From a one-person operation to a multinational, from a centuries-old trade to a brand-new workflow, the universal schema fits.
Why it exists
Operational knowledge — the how it's really done — is one of humanity's most valuable assets, and one of the easiest to lose. People retire. Apprentices move on. Companies pivot. Trades disappear. OpenPSL is small infrastructure for keeping that knowledge alive and shareable. Free, simple, and structured enough that machines can read it later if you want them to.
Quick start
From zero to a real procedure in under three minutes.
Sign in
One click with Google, Microsoft or GitHub.
Open the builder
Click + New procedure in the top bar.
Save & share
Save with Ctrl+S. Toggle public if you want others to read it.
Sign in
OpenPSL is OAuth-only. From the Sign in page choose one of three providers:
- Google — works with personal Gmail and any Google Workspace account.
- Microsoft — works with personal Outlook / Hotmail / Live, and with any Microsoft 365 / Azure AD work or school account.
- GitHub — for technical users.
The first time you choose a provider, OpenPSL creates your account automatically — there's no separate "register" step. There are no passwords to remember or recover. If you sign in later with a different provider but the same email address, the accounts are linked automatically.
Want to look around first? Browse the Explore page — public packs are readable without an account.
Your first procedure
- Click + New procedure in the top bar (or from your dashboard).
- Give it a title. Be specific. "Morning bread proof check" is much better than "Baking".
- Optionally add an Industry (e.g. "Bakery") and Domain (e.g. "Sourdough").
- Write a one-sentence Objective — what is this procedure for?
- Add two or three Steps. Drag the grip handle to reorder. Click the chevron on the right to expand a step and add a role, time estimate, or description.
- Save (button at the top of the right sidebar, or press Ctrl + S).
Publish or keep private
Every new procedure starts as Private — only you, signed in, can see it. In the right sidebar of the editor, toggle Public if you want it discoverable on the Explore page. Public packs can be read, copied (forked) and adapted by anyone with the link.
Core concepts
Procedure Packs
A Procedure Pack is one structured document describing one piece of work. It can be tiny ("Lock up at end of shift") or large ("Full clinical onboarding pathway"). Don't overthink the scope — start at whatever feels natural and split or merge later.
The universal schema
Every pack has the same shape under the hood. The visual builder edits it for you; you never have to touch JSON unless you want to export it. Here's the anatomy:
Title
What
Industry · Domain
Where
Objective
Why
Roles
Who
Inputs
What you need
Steps
What you do
Decision rules
If / then
Outputs
What you produce
Exceptions
Edge cases
Risks
Could go wrong
Controls
Keep risks down
You don't have to fill all of these. A 3-step procedure with one objective and no risks is a valid procedure. The structure exists to remind you what's possible, not to demand completeness.
Public vs private
Private packs are visible only to you when you're logged in. Public packs appear on the Explore page and can be opened by anyone with the URL. Flip a pack between the two at any time from the editor sidebar.
Anyone can read. Searchable on Explore. Forkable by others.
Only visible to you. Not in Explore. The URL doesn't work for other users.
Tags
Tags help you and others find packs. Add a few comma-separated tags in the editor sidebar — for example morning safety shift-handover. Tags are searchable and appear under each pack on Explore.
Versions & forks
Every time you save changes to a pack, OpenPSL bumps the version (you'll see v2, v3… on the view page). Public packs can be forked by any signed-in user — they get their own private copy that they can edit independently. The fork remembers where it came from.
Ancestor trail. Every fork stores a permanent, ordered list of the IDs of all its ancestors — the root pack, plus every intermediate fork between the root and itself. This trail is captured the moment the fork is created and never changes. On the view page of any fork you'll see a small breadcrumb like:
Each link in the breadcrumb is clickable and takes you to that ancestor's view page — as long as the ancestor is still public (or owned by you).
The chain survives deletions. If the author of an intermediate ancestor deletes their pack later, your descendant fork is unaffected — your content is yours and is not cascaded. The deleted ancestor is shown in the breadcrumb as deleted · cm4abc… with its short ID preserved, so the historical lineage stays auditable even when intermediate steps disappear. Ancestors above the deleted one remain clickable.
Fork count. Every public pack shows how many forks descend from it. The number appears on the Explore card, in the Explore modal header, and as a small chip in your dashboard cards — useful for seeing which of your packs are being adopted and adapted by the community.
Builder tour
A walk through every section of the editor, from top to bottom.
Title, industry, domain
At the top of the editor: one big title field and two metadata fields below. All three are required to save the pack, alongside objective, roles, inputs, steps, decisions and outputs. Exceptions, risks and controls stay optional — simple procedures don't need to force-fill them.
- Title * — be specific. "Daily safety check, mining shovel" beats "Safety check".
- Industry * — the broad sector your procedure belongs to. Examples: "Healthcare", "Banking & Financial Services", "Manufacturing", "Agriculture", "Software".
- Domain * — the sub-area within the industry. Examples: "Emergency room", "Underwriting", "Quality control", "Coffee harvesting".
Objective
One or two sentences about why this procedure exists. Not what it does — the deeper purpose. Example for an ER triage:
A clear objective helps the reader (and your future self) judge if they're applying the procedure to the right situation.
Roles
A list of who is involved. Each role has a name and an optional description. Define your roles here once, then pick them from a multi-select dropdown anywhere else in the pack that asks for a responsible party.
{
"roles": [
{ "name": "Triage nurse", "description": "Performs initial assessment." },
{ "name": "ER physician", "description": "Reviews red-flag cases." }
]
}Inputs
Things you need before you can start. Documents, tools, info, materials. Each input has a name, an optional note, and at least one responsible role (required) — pick from the roles you defined above. This forces explicit accountability: every input has a known owner.
- Patient ID (Triage nurse)
- Vital signs cart (Triage nurse)
- Weather forecast (Farm owner) — 12h outlook
Steps
The heart of any procedure. The Steps editor supports:
- Drag and drop to reorder. Grab the dot-handle on the left of each step.
- Sub-steps. Expand a step (chevron on the right) and click "Add sub-step". Useful when a step contains 2-3 micro-actions.
- Responsible role(s) — a multi-select dropdown that shows the roles you defined in the Roles section. Pick one or several; the chips render right inside the field.
- Estimated time — a structured input: a positive number plus a unit dropdown (milliseconds, seconds, minutes, hours, days, weeks, months, years). Old free-text values like "5 min" are parsed automatically on load.
- Description — 1-2 sentences elaborating the action. Keep the step name short and active ("Take vitals"); the description carries the detail.
Decisions
Real procedures rarely fork on a single yes/no. OpenPSL supports four kinds of decision rules — pick the one that matches the shape of the choice you're documenting. When you click + Add decision in the editor, you choose the type up front; the editor adapts to that shape.
If / then
A condition with YES and NO branches. Branches can themselves contain nested IFs up to 4 levels.
Matrix
A table with hierarchical row × column headers (up to 3 levels) and multi-value cells.
Formula
A calculation written as an expression. A scientific calculator pad helps build it.
Fallback
The "otherwise" / default action when no other rule applies.
If / then (with nesting)
A classic conditional. A condition plus what to do if true (YES) and what to do if false (NO). Either branch can itself contain nested IFs — up to four levels deep — so that you can capture complex eligibility checks, escalation chains, or troubleshooting decision trees without leaving the editor.
Example, brake-pad replacement:
{
"type": "if_then",
"condition": "Rotor below minimum thickness",
"ifTrue": "Replace rotor; do not refit pads onto worn rotor",
"ifFalse": "Proceed with pad replacement only"
}Decision matrix
When the choice depends on two or three dimensions at once (e.g. country × product × tier), use a matrix. Highlights:
- Hierarchical headers on both axes. A row or column header can have child headers, up to three levels deep. The rendered table uses rowspan and colspan so parents visibly group their children — e.g. A → A1, A2 → A11, A12.
- Multi-value cells. Each cell can hold up to 5 named values (e.g. rate: 4.5 + term: 24m) so a single matrix cell captures a small bundle of decisions.
- Copy & paste between cells. The icons inside each cell let you copy its content and paste into another cell, so you don't have to retype repeating values across the grid.
- Axis labels. Name each level of the row and column hierarchy (e.g. row levels: Country Region Branch).
Formula
Some decisions are math: a price, a dose, an interest rate. The formula editor takes a name + the expression itself + an optional note. A built-in scientific calculator pad sits below the expression input with quick-insert buttons for:
- Arithmetic: + − × ÷ % ^ = ≠
- Comparisons: < ≤ > ≥ ≈
- Grouping: ( ) [ ] { }
- Functions: √ x² xⁿ log ln exp abs
- Aggregation: min max sum avg round floor ceil
- Logic: if(…) AND OR NOT
- Constants: π e ∞
Example, a simple pricing rule:
{
"type": "formula",
"name": "Drug dose",
"expression": "round( weight_kg * 0.5 , 1 )",
"note": "Round to one decimal place"
}Fallback
The "otherwise" case. Use this when none of the previous rules in the pack apply — it's the catch-all default action. A fallback only carries one field, a description.
{
"type": "fallback",
"description": "Refer the case to the supervisor on shift"
}Outputs
What this procedure produces. Documents, deliverables, decisions, states — anything you can name.
- Triage level assigned
- Signed logbook entry
- Repaired vehicle returned to customer
Exceptions
When reality doesn't match the plan. Each exception has a "When" (the situation), an "Action" (what to do), and an optional list of responsible roles drawn from your Roles section.
[
{ "when": "Patient is pregnant",
"action": "Escalate one level higher",
"roles": ["ER physician"] },
{ "when": "Storm warning issued during the field walk",
"action": "Cancel the pick and shelter equipment",
"roles": ["Farm owner", "Picker lead"] }
]Risks
Things that could go wrong. Each risk has:
- A name (what could happen)
- Likelihood: low · medium · high
- Impact: low · medium · high
- An optional note
- Accountable role(s) — optional multi-select drawn from the Roles section. Useful for compliance audits where you need to show who owns each risk.
Below the list, the editor renders a 3×3 risk heatmap (likelihood × impact) with each risk colored by severity. Counts appear in the legend so you see at a glance how many risks fall in the high / medium / low buckets.
A risk doesn't have to be doom-and-gloom — it can be small ("Customer leaves before pickup").
Controls
Practical mitigations or safeguards. Each control has:
- A name and optional description.
- Linked risks (mitigates) — pick one or more risks from the dropdown to make the link between the mitigation and the risk explicit. The risks appear as chips on the control card.
- Executed by role(s) — optional multi-select. Who actually runs this control day-to-day?
Examples:
- Two-person signoff before closing the case
- Pre-shift checklist filed in the logbook
- Mandatory road test before handing the car back
Controls usually map one-to-one with risks. A good rule of thumb: for every high-impact risk, name at least one control that reduces it, and pick the relevant risk(s) from the link dropdown so the relationship is preserved in the exported JSON.
JSON schema
OpenPSL stores every Procedure Pack as a structured JSON document. You never have to touch this directly — the visual builder edits it for you — but knowing the shape helps if you want to:
- Move procedures between OpenPSL instances.
- Pipe procedures into another system (CMS, search, training data).
- Diff or version-control your procedures in Git.
Full reference
{
"title": "string (required)",
"objective": "string (required)",
"industry": "string (required)",
"domain": "string (required)",
"roles": [
{ "id": "string", "name": "string", "description": "string" }
],
"inputs": [
{ "id": "string", "name": "string", "note": "string",
"roles": ["role name", "..."] } // at least 1 required
],
"steps": [
{
"id": "string",
"name": "string",
"description": "string",
"roles": ["role name", "..."], // multi-select
"estimatedTime": "string (e.g. \"15 min\")",
"inputs": ["string"],
"outputs": ["string"],
"children": [ /* nested step */ ]
}
],
"decision_rules": [
// -- IF / THEN (with optional nested branches) --
{
"id": "string",
"type": "if_then",
"condition": "string",
"ifTrue": "string",
"ifFalse": "string",
"ifTrueChildren": [ /* nested if_then nodes (up to 4 levels) */ ],
"ifFalseChildren": [ /* nested if_then nodes (up to 4 levels) */ ]
},
// -- MATRIX --
{
"id": "string",
"type": "matrix",
"name": "string",
"rowAxes": ["Country", "Region", "Branch"],
"colAxes": ["Product", "Tier"],
"rowHeaders": [
{ "id": "string", "label": "string",
"children": [ /* same shape, up to 3 levels deep */ ] }
],
"colHeaders": [ /* same shape */ ],
"cells": {
"<rowLeafId>|<colLeafId>": {
"values": [
{ "id": "string", "key": "rate", "value": "4.5" }
]
}
},
"note": "string"
},
// -- FORMULA --
{
"id": "string",
"type": "formula",
"name": "string",
"expression": "round(weight_kg * 0.5, 1)",
"note": "string"
},
// -- FALLBACK --
{
"id": "string",
"type": "fallback",
"description": "Otherwise: refer to supervisor"
}
],
"outputs": [ { "id": "string", "name": "string", "note": "string" } ],
"exceptions": [
{ "id": "string", "when": "string", "action": "string",
"roles": ["role name", "..."] } // optional
],
"risks": [
{
"id": "string",
"name": "string",
"likelihood": "low | medium | high",
"impact": "low | medium | high",
"note": "string",
"roles": ["role name", "..."] // optional
}
],
"controls": [
{
"id": "string",
"name": "string",
"description": "string",
"mitigates": ["riskId", "..."],
"roles": ["role name", "..."] // optional
}
]
}Field types
- Required sections (9): title, industry, domain, objective, roles, inputs, steps, decision_rules, outputs. Text fields need a non-empty string; list sections need at least one valid item.
- Optional sections (3): exceptions, risks, controls. Add them when they fit your procedure; skip them when they don't.
- id is a short random string that OpenPSL generates. Keep it stable across edits to preserve references.
- List fields default to [] if absent.
- likelihood and impact are enums: "low", "medium", "high".
- visibility (public/private) is stored on the pack record, not inside the JSON.
Complete example
The "Coffee farmer daily harvest check" community template, exported as JSON:
{
"title": "Daily harvest readiness check (coffee farm)",
"objective": "Decide whether today's conditions and ripe-cherry coverage justify a pick.",
"industry": "Agriculture",
"domain": "Coffee harvesting",
"roles": [
{ "name": "Farm owner", "description": "Makes the final call." },
{ "name": "Picker lead", "description": "Mobilizes the picking crew." }
],
"inputs": [
{ "name": "Weather forecast", "note": "12h outlook" },
{ "name": "Last pick date" },
{ "name": "Sample baskets (3 random rows)" }
],
"steps": [
{
"name": "Walk three random rows",
"description": "Pick one branch per row. Count red vs. green cherries.",
"role": "Farm owner",
"estimatedTime": "20 min"
},
{
"name": "Check weather forecast",
"description": "Rain in next 8 hours? Note humidity.",
"role": "Farm owner",
"estimatedTime": "5 min"
},
{
"name": "Confirm pickers and transport",
"description": "Call lead, confirm number of pickers and truck slot.",
"role": "Picker lead",
"estimatedTime": "10 min"
}
],
"decision_rules": [
{
"condition": "Red cherries >= 70% AND no heavy rain in 8h",
"ifTrue": "Start picking by 7:00",
"ifFalse": "Delay 24h and recheck"
}
],
"outputs": [
{ "name": "Pick / no-pick decision" },
{ "name": "Crew brief (verbal)" }
],
"exceptions": [
{ "when": "Storm warning issued",
"action": "Cancel pick and shelter equipment" }
],
"risks": [
{ "name": "Picking under-ripe cherries reduces price",
"likelihood": "medium", "impact": "high" }
],
"controls": [
{ "name": "Three-row sampling rule",
"description": "Always sample 3 random rows before deciding." }
]
}Exporting
Every procedure can be exported in three formats. From the editor sidebar scroll to the Export panel; from the view page (or any public pack) the Export button shows the same options.
JSON
Full structured data. Use this when:
- Moving a procedure between OpenPSL instances.
- Feeding it into another tool (search index, training data, BPM).
- Version-controlling procedures alongside code in Git.
Markdown
Clean, human-readable Markdown rendering. Drop it into a wiki, README, Notion page, or a documentation site. The heading hierarchy and bullet structure are preserved.
Opens your browser's print dialog with a clean print layout (no nav, no sidebar — just the document). Save as PDF, print on paper, email it — whatever fits.
Search & discovery
Searching
The Explore page has a search box at the top. It searches across:
- Title
- Summary
- Industry
- Domain
- Tags
Searches are case-insensitive and substring-based — typing brake finds "brake pad replacement" and "Brake System".
Search by pack ID
Every Procedure Pack has a stable, unique pack ID (a short alphanumeric string like cm4abc123xyz). You can use this ID to find a pack precisely — useful when someone sends you a reference, when you're auditing relationships between forks, or when you need to track down an ancestor that may have been renamed.
How to copy an ID. On every Explore card (and in the modal that opens when you click a card) there is a small monospaced chip showing the start of the pack's ID — for example id: cm4abc123…. Click that chip to copy the full ID to your clipboard.
How to search. Paste the ID (full or any prefix of it) into the search box at the top of the Explore page. OpenPSL returns:
- The exact pack whose ID matches, AND
- Every descendant pack that has the queried ID somewhere in its ancestor trail — i.e. every fork, grand-fork or deeper that traces back to it.
That makes the ID search a one-shot way to see the whole downstream lineage of any pack you care about. Authors of popular community templates use this to see how their work has been adapted across the platform.
Tagging well
A few tips for tags that age well:
- Use lowercase, hyphenated tags — end-of-shift not End Of Shift.
- 3-5 tags per pack is usually enough. More becomes noise.
- Mix specific tags (coffee, harvest) with general ones (daily, agriculture).
- Avoid duplicating words that are already in the title.
Best practices
Write for a stranger
Imagine someone who's competent but new to your work picks up this pack. Would they get unstuck? If yes, you're done. If not, add one more detail — usually in the step's description or the procedure's objective.
Keep steps short
Step names should be a single small action: "Take vitals", "Refuel", "Stitch the welt". Long descriptions belong in the step's description field, not the step name. The eye should be able to scan the list of step names and grok the whole flow in 5 seconds.
Name roles clearly
"Senior nurse" beats "Person 1". The reader should be able to map roles to actual people on their team. If multiple roles overlap on one person in practice, name the role by its function, not by the person ("Closing shift cashier", not "Sarah").
Leave a hint of 'why'
Even one sentence in the Objective massively improves how useful the procedure is. People follow procedures better when they understand the point. "We do this so customers don't get a cold drink" beats no objective at all.
FAQ
How much does it cost?
OpenPSL is open source and free to self-host. The hosted version on openpsl.org is free for personal use.
How do I sign in?
OpenPSL is OAuth-only. There is no email + password form and no registration form. From the Sign in page choose Google, Microsoft or GitHub. The first time you sign in with a given provider, your account is created automatically. If you later sign in with a different provider but the same email, the accounts are linked.
Is private really private?
Yes. Private packs are visible only to your account when you're signed in. They are not listed on Explore, and opening their URL signed out (or as another user) returns a "not found" response.
Can I write in any language?
Absolutely. The schema is language-agnostic — write any text in any language. Search works in any language too. The UI itself is in English for now; translations are welcome contributions.
Can I import JSON?
Direct import via the UI isn't built yet. You can paste content into a new pack and the structure is forgiving, or use the API to POST a full JSON document to /api/procedures. Self-hosters can also use the npm run db:seed path to load templates in bulk.
How do I delete a pack?
Open the pack in the editor. Scroll the sidebar to the bottom and click the red Delete button. You'll be asked to confirm. Deleted packs and their version history are removed permanently.
What happens to forks if I delete the original?
Forks are independent copies, owned by the user who created them. They are not deleted when the original (or any intermediate ancestor) is deleted. The fork keeps its content, its history, and its place in the lineage.
What does change: in the fork's ancestor breadcrumb, the deleted ancestor is shown as deleted · cm4abc… instead of as a clickable link. The short ID is preserved so the historical chain remains auditable — useful for compliance, attribution, and for understanding where a procedure originated even after upstream packs disappear. Ancestors above the deleted one (e.g. the root, if a middle pack is deleted) stay fully visible and clickable.
The mirror image is also true: deleting your pack does not break or affect descendants others have forked from it. They keep what they built; only the link back to your original is marked deleted in their breadcrumb.
Ready to document something?
The best procedure is the one that exists. Open the builder and capture one task you do — five minutes is enough for a first version.