An SOP template you edit in one place and share as one link

Write the standard operating procedure as a web page, send its link once, and fix a step by editing the page. Everyone who clicks sees the current steps.

An SOP template is a page with the same parts every time: title, purpose, scope, roles, numbered steps and a revision history. Write it as a web page, share one link, and fix a step by editing the page. The link stays the same.

A guide from a training-software company names the usual problem as SOPs "scattered or duplicated across tools, so no one can tell which version is current." One link answers that question: it opens the page as it stands now.

Try the page below. Tick the steps as you work, open a Why note, then press Reset.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SOP: Process a supplier invoice</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; color: #1f2937; background: #f3f4f6; line-height: 1.45; }
  .sheet { max-width: 680px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 22px; box-shadow: 0 1px 4px rgba(0,0,0,.08); }
  .code { font-size: 12px; letter-spacing: .5px; text-transform: uppercase; color: #6b7280; margin: 0 0 4px; }
  h1 { margin: 0 0 12px; font-size: 22px; }
  h2 { margin: 22px 0 8px; font-size: 15px; text-transform: uppercase; letter-spacing: .4px; color: #374151; }
  .meta { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin: 0 0 6px; }
  .meta div { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px 10px; font-size: 13px; }
  .meta b { display: block; font-size: 11px; color: #6b7280; text-transform: uppercase; letter-spacing: .4px; font-weight: 600; }
  p { margin: 0 0 8px; font-size: 14.5px; }
  ul.plain { margin: 0; padding-left: 20px; font-size: 14.5px; }
  .prog { display: flex; align-items: center; gap: 10px; margin: 6px 0 10px; font-size: 13px; color: #374151; }
  .bar { flex: 1; height: 8px; background: #e5e7eb; border-radius: 99px; overflow: hidden; }
  .bar i { display: block; height: 100%; width: 0; background: #16a34a; transition: width .2s; }
  .prog button { font: inherit; font-size: 13px; padding: 5px 10px; border: 1px solid #d1d5db; background: #fff; border-radius: 6px; cursor: pointer; }
  ol { list-style: none; margin: 0; padding: 0; counter-reset: s; }
  ol li { counter-increment: s; display: grid; grid-template-columns: 28px 1fr; gap: 10px; padding: 11px 0; border-top: 1px solid #e5e7eb; }
  ol li::before { content: counter(s); grid-column: 1; grid-row: 1; width: 26px; height: 26px; border-radius: 50%; background: #eef2ff; color: #3730a3; font-size: 13px; font-weight: 700; display: grid; place-items: center; }
  ol li.done::before { background: #dcfce7; color: #166534; }
  .body { grid-column: 2; }
  label { display: flex; gap: 9px; align-items: flex-start; font-size: 15px; cursor: pointer; }
  label input { width: 20px; height: 20px; margin: 1px 0 0; flex: none; }
  li.done label span { text-decoration: line-through; color: #9ca3af; }
  details { margin: 6px 0 0 29px; font-size: 13.5px; color: #4b5563; }
  summary { cursor: pointer; color: #2563eb; }
  details p { margin: 4px 0 0; font-size: 13.5px; }
  .role { display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 99px; font-size: 11px; background: #fef3c7; color: #92400e; vertical-align: 1px; }
  table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
  th, td { text-align: left; padding: 7px 8px; border-top: 1px solid #e5e7eb; vertical-align: top; }
  th { font-size: 12px; color: #6b7280; font-weight: 600; }
  .ok { background: #f0fdf4; border-left: 4px solid #16a34a; padding: 9px 12px; border-radius: 6px; font-size: 14px; }
  @media (max-width: 480px) { body { padding: 10px; } .sheet { padding: 16px; } .meta { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="sheet">
  <p class="code">SOP-FIN-004</p>
  <h1>Process a supplier invoice</h1>

  <div class="meta">
    <div><b>Owner (role)</b>Accounts payable lead</div>
    <div><b>Version</b>3.1</div>
    <div><b>Last reviewed</b>1 October 2026</div>
    <div><b>Next review</b>1 April 2027</div>
  </div>

  <h2>Purpose</h2>
  <p>Every supplier invoice is checked, approved and queued for payment once, by the right person, before its due date.</p>

  <h2>Scope</h2>
  <p>Covers invoices received by email or post for goods and services. It does not cover staff expense claims (see SOP-FIN-007).</p>

  <h2>Before you start</h2>
  <ul class="plain">
    <li>Access to the finance inbox and the payments tool</li>
    <li>The matching purchase order number</li>
  </ul>

  <h2>Steps</h2>
  <div class="prog">
    <span id="count">0 of 7 done</span>
    <span class="bar"><i id="fill"></i></span>
    <button type="button" id="reset">Reset</button>
  </div>
  <ol id="steps">
    <li><div class="body"><label><input type="checkbox"><span>Open the invoice and check supplier name, date and total.</span></label>
      <details><summary>Why</summary><p>A wrong name or date can make a payment bounce back.</p></details></div></li>
    <li><div class="body"><label><input type="checkbox"><span>Find the purchase order and confirm the amounts match.<em class="role">AP clerk</em></span></label>
      <details><summary>If they do not match</summary><p>Do not approve. Email the buyer, copy the supplier, and note the date you asked.</p></details></div></li>
    <li><div class="body"><label><input type="checkbox"><span>Enter the invoice in the payments tool and attach the PDF.</span></label></div></li>
    <li><div class="body"><label><input type="checkbox"><span>Send it to the budget holder for approval.<em class="role">Budget holder</em></span></label></div></li>
    <li><div class="body"><label><input type="checkbox"><span>When approved, set the payment date to the due date.<em class="role">AP lead</em></span></label>
      <details><summary>Why the due date</summary><p>Paying on the due date keeps the cash in the account until then.</p></details></div></li>
    <li><div class="body"><label><input type="checkbox"><span>Reply to the supplier: "Received, payment on [date]."</span></label></div></li>
    <li><div class="body"><label><input type="checkbox"><span>File the PDF in the supplier folder and mark the invoice as queued.</span></label></div></li>
  </ol>

  <h2>Expected result</h2>
  <p class="ok">The invoice shows as queued, with an approver and a payment date, and the supplier has been told.</p>

  <h2>If something goes wrong</h2>
  <table>
    <tr><th>Problem</th><th>What to do</th></tr>
    <tr><td>No purchase order</td><td>Ask the budget holder to raise one, then restart at step 2.</td></tr>
    <tr><td>Approver away</td><td>Send it to their named deputy.</td></tr>
  </table>

  <h2>Revision history</h2>
  <table>
    <tr><th>Version</th><th>Date</th><th>What changed</th></tr>
    <tr><td>3.1</td><td>1 Oct 2026</td><td>Step 6: tell the supplier the payment date.</td></tr>
    <tr><td>3.0</td><td>2 Mar 2026</td><td>Approval moved to the budget holder.</td></tr>
  </table>
</div>

<script>
  // Tick steps as you work; the counter and the bar follow.
  const items = document.querySelectorAll('#steps li');
  const boxes = document.querySelectorAll('#steps input');
  function update() {
    let n = 0;
    boxes.forEach((b, i) => {
      items[i].classList.toggle('done', b.checked);
      if (b.checked) n++;
    });
    document.getElementById('count').textContent = n + ' of ' + boxes.length + ' done';
    document.getElementById('fill').style.width = (n / boxes.length * 100) + '%';
  }
  boxes.forEach(b => b.addEventListener('change', update));
  document.getElementById('reset').addEventListener('click', () => {
    boxes.forEach(b => b.checked = false);
    update();
  });
  update();
</script>
</body>
</html>
A full SOP on one page. Steps tick off, notes open under the step, and the revision history sits at the bottom.

Each part of the page has one job. The header says who owns the SOP and when it was last reviewed. The steps are short and numbered. The expected result tells the reader when they are done.

What goes in an SOP template

Stack Overflow's guide to writing an SOP lists six parts: title and purpose, scope, responsibilities, procedure, references, and revision history. The page above follows that list and adds a few extras that help in daily use.

  • Owner as a role. "Accounts payable lead" still works after a team change. A person's name does not.
  • Review date. One line that tells the reader how fresh the page is.
  • Before you start. The access and papers the reader needs, so they do not stop on step 3.
  • Expected result. What the finished job looks like.
  • If something goes wrong. The two or three problems that really happen, and what to do.

Keep one action in each step, starting with a verb. If a step needs the word "and" twice, split it. Put the reason in a Why note, not in the step.

Make it with AI

You do not have to build the page by hand. Paste this into ChatGPT, Claude or Gemini and swap in your own procedure:

Make one self-contained HTML file for a standard operating procedure (SOP).
Title: Process a supplier invoice. Code SOP-FIN-004, version 3.1, last reviewed 1 October 2026, next review 1 April 2027. Owner: a role, "Accounts payable lead".
Sections in this order: purpose (one sentence), scope (say what it does not cover), before you start, numbered steps, expected result, if something goes wrong (table), revision history (table).
Rules: one action per step, starting with a verb. Add a role tag to a step only when a different role does it. Give each step a tick box, show "x of N done" and a Reset button. Use only the roles and steps I give you. Write TBC where something is missing. No external images, fonts or scripts. It must read well on a phone, so include a viewport meta tag.
Steps: check the invoice details; match the purchase order; enter it in the payments tool; send to the budget holder; set the payment date; tell the supplier; file the PDF.

Then check five things before you share it:

  • The steps are in your order, and the count in the counter matches the number of steps.
  • Every role is one you gave. AI tools sometimes invent a plausible job title.
  • Gaps say TBC, not an invented deadline or system name.
  • The version, the review date and the revision history agree with each other.
  • Open it on your phone. The steps should not run off the edge of the screen.

Copy the code the AI gave you. Paste it into a NOS document and choose Create share link. HTML to link walks through it.

From the AI chat to one link: ask, check, create the share link, post it once.
From the AI chat to one link: ask, check, create the share link, post it once.

The recipient gets a normal page in their browser. It opens on a phone as well as a laptop, needs no account, and cannot be edited by the person reading it. The page renders as written and its scripts run.

What the other person sees: the SOP in a phone browser, no account, read only.
What the other person sees: the SOP in a phone browser, no account, read only.

In Slack. As of October 2026, Slack's help says you can pin a message from its three-dot menu, or add a link from the plus sign at the top. Menu names differ by plan. More in sending an HTML file in Slack.

In email. A link is text, so you can paste it into a message or an onboarding email. See HTML in onboarding docs for the same idea applied to a new starter's pack.

On paper. Print the link as a QR code and stick it at the workstation. The paper does not change, but the page it opens does. QR code links covers it.

Change a step without sending a new version

Three files on the left, one link on the right. The link went out once.
Three files on the left, one link on the right. The link went out once.

Say step 6 is new: the supplier must be told the payment date. With files, you replace the document and hope people delete the old one. With the link, you edit one step, add a line to the revision history, and send nothing.

The link always shows the current page. A reader cannot open the version from March, so keep a revision history table in the page, with the date and what changed.

If your company needs approvals or signed copies, keep those in your own records system as well. A share link does not do either.

In View mode, the owner's ticks and added rows are saved. A visitor's are not, so each person's ticks are theirs for that visit only.

One SOP, a list for each role

Some procedures involve several people. Each reader wants their own steps, not the whole page. This page lets the reader pick a role, and turns what is shown into plain text for an email.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SOP: Set up a new starter's access</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; color: #1f2937; background: #f3f4f6; line-height: 1.45; }
  .sheet { max-width: 680px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 1px 4px rgba(0,0,0,.08); }
  .code { font-size: 12px; letter-spacing: .5px; text-transform: uppercase; color: #6b7280; margin: 0 0 4px; }
  h1 { margin: 0 0 6px; font-size: 21px; }
  .sub { margin: 0 0 14px; font-size: 14px; color: #6b7280; }
  .tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
  .tabs button { font: inherit; font-size: 14px; padding: 7px 12px; border: 1px solid #d1d5db; background: #fff; border-radius: 99px; cursor: pointer; }
  .tabs button[aria-pressed=true] { background: #1d4ed8; border-color: #1d4ed8; color: #fff; }
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
  th, td { text-align: left; padding: 9px 8px; border-top: 1px solid #e5e7eb; vertical-align: top; }
  th { font-size: 12px; color: #6b7280; font-weight: 600; }
  td.n { width: 34px; font-weight: 700; color: #3730a3; }
  td.w { width: 96px; white-space: nowrap; }
  .pill { display: inline-block; padding: 1px 8px; border-radius: 99px; font-size: 12px; background: #eef2ff; color: #3730a3; }
  .pill.it { background: #dcfce7; color: #166534; }
  .pill.mgr { background: #fef3c7; color: #92400e; }
  .pill.new { background: #fce7f3; color: #9d174d; }
  tr.dim { display: none; }
  .note { margin: 14px 0 0; font-size: 13px; color: #6b7280; }
  .out { margin-top: 12px; }
  .out button { font: inherit; font-size: 14px; padding: 7px 12px; border: 0; border-radius: 6px; background: #1f2937; color: #fff; cursor: pointer; }
  textarea { display: block; width: 100%; height: 150px; margin-top: 8px; font: 13px/1.4 ui-monospace, Consolas, monospace; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px; }
  @media (max-width: 480px) { td.w { width: 74px; white-space: normal; } }
</style>
</head>
<body>
<div class="sheet">
  <p class="code">SOP-HR-012 · Version 2.0 · Owner: Office manager</p>
  <h1>Set up a new starter's access</h1>
  <p class="sub">Show the steps for one role, or all of them. Each person sees only their own list.</p>

  <div class="tabs" id="tabs">
    <button type="button" data-r="all" aria-pressed="true">All steps</button>
    <button type="button" data-r="Manager" aria-pressed="false">Manager</button>
    <button type="button" data-r="IT" aria-pressed="false">IT</button>
    <button type="button" data-r="New starter" aria-pressed="false">New starter</button>
  </div>

  <table id="tbl">
    <tr><th>#</th><th>Who</th><th>Step</th></tr>
    <tr data-r="Manager"><td class="n">1</td><td class="w"><span class="pill mgr">Manager</span></td><td>Send the access request form at least 5 working days before day one.</td></tr>
    <tr data-r="IT"><td class="n">2</td><td class="w"><span class="pill it">IT</span></td><td>Create the email account and add it to the team group.</td></tr>
    <tr data-r="IT"><td class="n">3</td><td class="w"><span class="pill it">IT</span></td><td>Prepare the laptop and label it with the starter's name.</td></tr>
    <tr data-r="Manager"><td class="n">4</td><td class="w"><span class="pill mgr">Manager</span></td><td>Book a 30-minute welcome call for day one.</td></tr>
    <tr data-r="New starter"><td class="n">5</td><td class="w"><span class="pill new">New starter</span></td><td>Sign in, set a new password and turn on two-step sign-in.</td></tr>
    <tr data-r="New starter"><td class="n">6</td><td class="w"><span class="pill new">New starter</span></td><td>Reply to the welcome email to confirm everything opens.</td></tr>
    <tr data-r="Manager"><td class="n">7</td><td class="w"><span class="pill mgr">Manager</span></td><td>If anything is missing, tell IT the same day.</td></tr>
  </table>
  <p class="note" id="note"></p>

  <div class="out">
    <button type="button" id="txt">Show as plain text</button>
    <textarea id="box" readonly placeholder="The list appears here, ready to paste into an email."></textarea>
  </div>
</div>

<script>
  // Filter the table rows by role, and turn what is visible into plain text.
  const rows = [...document.querySelectorAll('#tbl tr[data-r]')];
  const buttons = document.querySelectorAll('#tabs button');
  let role = 'all';

  function show() {
    let n = 0;
    rows.forEach(r => {
      const on = role === 'all' || r.dataset.r === role;
      r.classList.toggle('dim', !on);
      if (on) n++;
    });
    buttons.forEach(b => b.setAttribute('aria-pressed', b.dataset.r === role));
    document.getElementById('note').textContent = n + ' of ' + rows.length + ' steps shown';
  }

  buttons.forEach(b => b.addEventListener('click', () => { role = b.dataset.r; show(); }));

  document.getElementById('txt').addEventListener('click', () => {
    const lines = rows.filter(r => !r.classList.contains('dim')).map(r => {
      const c = r.children;
      return c[0].textContent + '. [' + c[1].textContent + '] ' + c[2].textContent;
    });
    document.getElementById('box').value =
      'Set up a new starter\'s access (SOP-HR-012, v2.0)\n' + lines.join('\n');
  });
  show();
</script>
</body>
</html>
One SOP with three roles. Pick a role to see only its steps, then show the list as plain text.

The filter only hides rows. The full table is still on the page, so the SOP stays one document with one owner.

Ways to share an SOP compared

Way to share What people get When a step changes Fits best
Attach a Word or PDF file A file to download Send a new file; old copies stay in inboxes A one-off, signed copy
A file in a shared folder The file, from the folder Replace the file; saved copies stay as they were Documents your IT team manages
Share link to the page A page in the browser, phone included Edit the page; same link SOPs that change
Link pinned in a channel The link at the top of the channel Same link, nothing to pin again A team that lives in chat
QR code at the workstation The link, printed The paper stays, the page is current Shop floor, desk or equipment

For why a link beats a file in a thread, see why links beat attachments. For a tick-off list with no other text, HTML checklist is the shorter version.

When it does not work

What you see Cause Fix
People follow an old step They saved a file copy, or open an older link Give out only the page link, and ask people to reload
Steps are out of order The AI re-sorted them Say "keep my order" in the prompt
A role you never gave appears The AI filled a gap Tell it to use only your roles and write TBC
Version and history disagree You changed one and not the other Update the header and the history table together
A visitor's tick does not stay Visitors' changes are not saved Treat ticks as personal for that visit
A picture from another site is missing Images from other sites are blocked Use an inline SVG or a coloured block

If a form on the page tries to send answers to a server, that is blocked too. Collect replies by email or in the thread.

Questions people ask

What should an SOP template include?

A title and purpose, the scope, who is responsible, the numbered steps, references, and a revision history. Add an owner role, a review date and an expected result so the reader can tell when the job is done.

How do I keep old versions of an SOP?

The link shows the current page, so readers never land on an old step. If you also need to keep old versions, keep a revision history table inside the page, and save a copy of the code each time. Check what your own rules require for approvals and records.

Does the link change when I edit a step?

No. Editing the code in your NOS document keeps the same link. Anyone who opens it afterwards sees the new version, including people who saved the link weeks ago.

Can my team edit the SOP page?

Not through the link. People who open a share link can read the page but cannot change it. They tell the owner, and the owner edits the page.

How do I share an SOP in Slack?

Post the link in the channel, then pin the message or add the link from the plus sign at the top of the channel. Menu names differ by plan, so check on your own screen.

Keep reading