Author SHA1 Message Date
blog-dev b6fea27963 feat: surface Cyber Essentials certification on homepage hero and footer 2026-09-11 19:45:06 +00:00
blog-dev aa1f015f5a docs(blog): punchier LinkedIn draft for non-DC users - pain-first, no jargon
publish / Build-and-publish (push) Successful in 6m39s
2026-09-04 09:29:20 +00:00
blog-dev 79b62e4cfc docs(blog): add source LinkedIn post comment for the v7.13 article
publish / Build-and-publish (push) Successful in 6m33s
2026-09-04 08:06:46 +00:00
blog-dev 73d4796638 fix(blog): show sasdemo instead of root user in audit demo screenshot
publish / Build-and-publish (push) Successful in 6m36s
2026-09-04 07:51:32 +00:00
blog-dev 7267af58c4 ci: drop npm cache from setup-node - save/restore costs exceed the benefit at this repo size
publish / Build-and-publish (push) Successful in 7m0s
2026-09-04 07:23:58 +00:00
blog-dev e5dfcd84da feat(blog): add audit-column demo image showing the IF formula resolving live
publish / Build-and-publish (push) Successful in 13m17s
2026-09-04 01:28:42 +00:00
blog-dev 02295334b3 fix(blog): restore hardening and polishing phrasing in v7.13 post
publish / Build-and-publish (push) Successful in 13m11s
2026-09-04 01:09:48 +00:00
blog-dev e7a538ed9a feat(blog): link getdata and postedit hook to code.datacontroller.io
publish / Build-and-publish (push) Successful in 15m42s
2026-09-04 00:53:20 +00:00
blog-dev 60a19dda89 fix(blog): correct PK live-formula wording in v7.13 post
publish / Build-and-publish (push) Successful in 15m16s
2026-09-04 00:36:58 +00:00
blog-dev b415793eec fix(blog): trim intra-release fix churn from the also-in-release list
publish / Build-and-publish (push) Successful in 15m40s
2026-09-04 00:20:36 +00:00
blog-dev d352ea8fea fix(blog): re-encode v7.13 images to bust poisoned CDN edge cache entries
publish / Build-and-publish (push) Successful in 15m47s
The /static/* path sits behind a Cloudflare cache-everything rule with a
62-day edge TTL. An image fetched mid-deploy can pin a bad (truncated/404)
response at an edge POP. Re-encoding produces new content-hashed URLs so
every variant is fetched fresh from origin. Pixels are identical.
2026-09-04 00:19:32 +00:00
blog-dev 9e04cd4220 feat(blog): v7.13 release post - formulas & regex with live screenshots
publish / Build-and-publish (push) Successful in 15m47s
2026-09-03 23:56:29 +00:00
blog-dev 181c3ae834 fix(feed): update LinkedIn source copy - community edition, not 5-user free tier
publish / Build-and-publish (push) Successful in 13m5s
2026-08-27 17:04:13 +01:00
blog-dev 0ec0789ac4 feat(feed): five lines of defence post with cover image
publish / Build-and-publish (push) Successful in 13m1s
2026-08-27 16:43:07 +01:00
blog-dev 2e5fc4a368 feat(feed): legacy data ingestion post with cover image
publish / Build-and-publish (push) Successful in 12m56s
2026-08-24 16:37:11 +01:00
blog-dev 3d66f91820 fix(feed): correct docs link to /restore/
publish / Build-and-publish (push) Successful in 12m54s
2026-08-20 22:26:03 +01:00
blog-dev 5cdfadf75c feat(feed): use ctrl-z as cover, move trains meme into post body
publish / Build-and-publish (push) Successful in 13m0s
2026-08-20 22:20:56 +01:00
hermes fef2c3c12e chore: li update
publish / Build-and-publish (push) Successful in 12m58s
2026-08-20 22:14:47 +01:00
blog-dev b50efc01e2 feat(feed): expand rollback post LinkedIn copy, drop version refs
publish / Build-and-publish (push) Successful in 12m52s
2026-08-20 21:50:27 +01:00
blog-dev de23966721 feat(feed): add rollback data changes post with meme 2026-08-20 21:06:43 +01:00
4gl 9121bf900d fix: contact image
publish / Build-and-publish (push) Successful in 13m2s
2026-08-17 20:14:26 +01:00
4gl 56175353d6 feat: sas projects / excel
publish / Build-and-publish (push) Successful in 12m56s
2026-08-17 19:51:20 +01:00
4gl 26b4c55ecd feat: horror stories
publish / Build-and-publish (push) Successful in 13m19s
2026-08-15 09:47:06 +01:00
4gl 5be23393d6 fix: dc link
publish / Build-and-publish (push) Successful in 12m56s
2026-08-13 13:52:18 +01:00
4gl bf3ef8bab1 fix: links
publish / Build-and-publish (push) Successful in 12m51s
2026-08-13 13:46:43 +01:00
4gl 729437308a fix: feed post git data
publish / Build-and-publish (push) Successful in 13m19s
2026-08-13 13:28:30 +01:00
4gl 244829edf9 feat: post
publish / Build-and-publish (push) Successful in 12m58s
2026-08-12 10:32:04 +01:00
4gl b51c831b53 chore: ai docs
publish / Build-and-publish (push) Successful in 13m28s
2026-08-11 10:47:35 +01:00
4gl 8e0b28ed93 feat: libinfo feed post
publish / Build-and-publish (push) Successful in 13m33s
2026-08-11 09:13:04 +01:00
4gl 5b6575ae8e fix: small edits + ai notes
publish / Build-and-publish (push) Successful in 12m53s
2026-08-10 18:42:49 +01:00
4gl 9dcedfffb0 feat: business data capture feed post
publish / Build-and-publish (push) Successful in 12m55s
2026-08-10 18:28:16 +01:00
4gl 34846e5a4c fix: formatting
publish / Build-and-publish (push) Successful in 12m51s
2026-08-09 18:12:42 +01:00
4gl 26304f3c73 feat: fsedit feed post
publish / Build-and-publish (push) Successful in 13m9s
2026-08-09 18:00:11 +01:00
4gl 14b745e2e7 fix: agent tidy up 2026-08-09 17:39:33 +01:00
59 changed files with 1247 additions and 396 deletions

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
# Domain Docs
How to consume this repo's domain documentation when exploring the codebase.
## Before exploring, read these
- **`CONTEXT.md`** at the repo root - the domain glossary and orientation.
- **`.agents/adr/`** - read the Architecture Decision Records that touch the area you're about to work in.
If any of these files don't exist, proceed silently. Don't flag their absence or suggest creating them upfront; they are created lazily when terms or decisions actually get resolved.
## File structure
```
/
├── CONTEXT.md
├── .agents/
│ ├── adr/
│ │ ├── 0001-<decision-slug>.md
│ │ └── 0002-<decision-slug>.md
│ ├── docs/
│ └── skills/
└── src/
```
## Use the glossary's vocabulary
When your output names a domain concept (an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary avoids.
If the concept you need isn't in the glossary yet, that's a signal - either you're inventing language the project doesn't use (reconsider), or there's a real gap (note it).
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0001 (...) - but worth reopening because..._
+19
View File
@@ -0,0 +1,19 @@
# Issue tracker: Local Markdown
Issues and specs (you may know a spec as a PRD) for this repo live as markdown files in `.scratch/`.
## Conventions
- One feature per directory: `.scratch/<feature-slug>/`
- The spec is `.scratch/<feature-slug>/spec.md`
- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` - never a single combined tickets file
- Triage state is recorded as a `Status:` line near the top of each issue file
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
## When a skill says "publish to the issue tracker"
Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
## When a skill says "fetch the relevant ticket"
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
+63
View File
@@ -0,0 +1,63 @@
# LinkedIn Post Writing Guide
Guidance for drafting LinkedIn posts promoting Data Controller (datacontroller.io).
## Audience
Our readers are typically:
- **Technical managers and platform owners** in regulated industries (Banking, Insurance, Pharma) running enterprise SAS® estates.
- **Data governance / data quality leads** dealing with End User Computing (EUC) risk and audit requirements.
- **SAS developers and administrators** who maintain data pipelines and care about automation, security and auditability.
- Occasionally **C-level / procurement** - keep business value (OpEx, risk reduction, compliance) visible alongside technical detail.
Implications:
- Assume competence - no need to explain what ETL, SAS, or a data warehouse is.
- Speak to their pains: regulatory reporting deadlines, audit trails, hand-cranked ETL, EUC sprawl, OpEx, blame-games between business and IT.
- Professional but direct tone. Avoid hype, buzzword stuffing, and exclamation-mark overload.
## Structure
1. **Hook** - first 1-2 lines must stand alone; LinkedIn truncates the rest behind "…see more". State the pain or the claim, not the product.
2. **Problem** - short paragraphs describing the pain in the reader's context.
3. **Belief / shift** - what we believe the world should look like.
4. **Solution** - introduce Data Controller, one or two sentences on what it does (zero-code data capture, review/approve workflow, audit trail, loads into SAS / any database).
5. **Outcome** - the concrete result (eliminated ETL flows, reduced OpEx, full lineage).
6. **CTA** - single call to action, typically "book a Discovery Call - link in the comments below 👇". LinkedIn deprioritises posts with external links, so put URLs in the first comment, not the body.
## Formatting capabilities and tricks
LinkedIn posts are plain text - there is no rich-text editor, so **no markdown**. Bold, italics and other styling you see in posts are faked with Unicode characters:
- **Faux bold / italic / monospace via Unicode Mathematical Alphanumeric Symbols** - e.g. `𝐛𝐨𝐥𝐝` (U+1D401+), `𝑖𝑡𝑎𝑙𝑖𝑐` (U+1D456+), `𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎` (U+1D670+). Tools like YayText / LingoJam generate these. **Use with extreme restraint:** they break screen readers and accessibility tools, are not searchable (LinkedIn search treats them as different characters, so keywords in faux-bold won't be found), and can render as boxes on some devices. Our default: avoid them; use CAPS sparingly for emphasis instead. If a hook or single key term really needs to pop, one short faux-bold phrase is the acceptable ceiling.
- **Bullets** - no native lists; use literal characters: `-`, `•`, `✓`, `→`, numbered `1.` etc. Each on its own line.
- **Whitespace** - blank lines survive and are the primary layout tool; use them aggressively (1-3 sentence paragraphs). LinkedIn collapses multiple blank lines beyond one, so a single blank line is the unit.
- **Emoji** - work natively and render fine; use sparingly and professionally (👇 for the CTA, occasional ✅). Avoid emoji walls.
- **Links** - URLs in the body get auto-linked but LinkedIn deprioritises posts containing external links; put the URL in the first comment and point to it ("link in the comments 👇"). Tag people/companies with `@mention` (native, drives reach) where relevant.
- **Truncation** - only the first ~3 lines (~210 characters on mobile) show before "…see more". The hook must earn the click; never waste the first line on a greeting or context-setting.
- **Special characters** - Unicode symbols like → ✅ ⏱ work fine and can structure a post; avoid exotic characters that some fonts render as tofu (□).
- **No nested structure** - no headings, tables, quotes or code blocks. Simulated section breaks (a line of `—` or just blank space) are the only separators.
## Formatting rules
- **Hashtags are written as `#tag`** (e.g. `#endusercomputing`, `#excel`, `#datagovernance`, `#sas`, `#masterdata`). Never write `hashtag#tag` - that is an artefact of copy-pasting from LinkedIn's editor UI and must be stripped/normalised to `#tag` before posting or republishing.
- 3-5 hashtags, all lowercase, placed at the end on their own lines.
- Use regular dashes (`-`), not em-dashes (`—`).
- Short paragraphs (1-3 sentences) with blank lines between them - LinkedIn has no markdown.
- No markdown syntax (no `**bold**`, no `[links](url)`) - LinkedIn renders plain text. Use CAPS sparingly for emphasis instead.
- ® symbol on first mention of SAS® is fine; don't repeat it every time.
- Keep total length under ~1,300 characters where possible; longer posts are fine when the story warrants it, but the hook still has to earn the "see more" click.
## Media
- Always attach an image - posts with images get substantially more reach. Use the same image as the feed post's `previewImg` where one exists.
- **Optimum image size: 1200 x 627 px (1.91:1 landscape)** - LinkedIn's recommended size for feed images and link previews, displays uncropped on desktop and mobile. Square (1080 x 1080) and portrait (1080 x 1350, 4:5) also work and take up more feed real estate on mobile; avoid narrow/tall images beyond 4:5 as they get cropped. Keep key content away from the edges.
- Product screenshots work well for feature announcements; flat branded illustrations for thought-leadership posts.
## Relationship to the site Feed
- Feed posts (`content/feed/`) and LinkedIn posts are siblings, not copies: the site version may contain markdown links and omits hashtags; the LinkedIn version uses plain text, `#tag` hashtags, and "link in the comments".
- When converting site → LinkedIn: strip markdown, convert links to a comment-based CTA, append hashtags.
- When converting LinkedIn → site: strip `hashtag#` artefacts and hashtags, add proper markdown links, add front matter per the `add-feed-post` skill.
- **Keep them in sync:** whenever a feed post is modified, update its `Source LinkedIn post:` comment in the same change (same points, same claims, same ordering of lists) - and vice versa. A stale LinkedIn comment means the next social post is drafted from outdated copy.
+102
View File
@@ -0,0 +1,102 @@
# Data Controller - Sales & Positioning Playbook
Reference for writing marketing, blog, feed, and sales copy. Covers pain points, target audience, value proposition, packaging, sales motion, and objection handling. Synthesised from the marketing site (`datacontroller.io`), product docs (`docs.datacontroller.io`), and product glossary (`dc/CONTEXT.md`).
## The product in one paragraph
Data Controller for SAS® is a SAS-Powered web application that lets business users safely add, modify and delete data in SAS datasets and databases. Every change goes through an edit-stage-approve workflow: edits are staged, validated at the point of entry, reviewed as a diff (new / changed / deleted rows), approved by a data owner, then loaded - with a full audit trail of who changed what, when, and why. It runs on SAS Viya, SAS 9 EBI and SASjs Server, entirely on-prem / offline (no external network calls). It can write to any database reachable via a SAS libname engine.
## The problem (pain points)
SAS users routinely need to load or fix data manually - steering tables, parameters, mappings, corrections, regulatory adjustments. The status-quo approaches all hurt:
- **Network drive + batch job** - drop a CSV/Excel on a share, wait for a nightly job. Every new input means a new job to build, test, document and deploy. File-format drift breaks batches at the worst times (month end).
- **Direct SAS code / Enterprise Guide edits** - requires granting end users MODIFY access to sensitive data locations. High risk of locking datasets, no audit trail, no approval.
- **Asking a DBA / IT to run a script** - change requests, queues, delays, blame games between business and IT.
- **Bespoke in-house capture apps** - years of development and testing, and a "black box" nobody wants to maintain.
- **EUC sprawl** - uncontrolled Excel/VBA applications on network shares: opaque access, no version control, no lineage, audit failure. Regulators care (SOX, BCBS, national DQ circulars). Fines are real: Morgan Stanley $5m (2020), Citibank $400m (2020), DTCC £350k (2021).
The five measurable cost areas (see blog post "ROI and Payback"):
1. **Development time** - reduced to zero (zero-code, config-driven; the grid is generated dynamically from table metadata).
2. **Deployment time** - reduced to zero (config change in the UI, not a code release).
3. **Batch incidents** - invalid data cannot be submitted (validated on arrival).
4. **Data quality issues** - schema-based automatic rules plus configurable frontend validations and backend hook scripts.
5. **Compliance costs** - single source of truth, full audit trail, Excel originals retained with each submission; no reperformed audits of unsecured EUC code.
## Target audience
**Verticals:** regulated industries with large SAS estates - Banking, Insurance, Pharma / Life Sciences. Typical use cases: regulatory reporting adjustments, actuarial / bitemporal reporting (Allianz), DWH steering-table management (AFA försäkring), data catalogs and lineage (Siemens Healthineers), BICC / BI competency centres (DER Touristik).
**Personas:**
- **Economic buyer:** Head of BI / Data Platform owner, CDO, Head of Regulatory Reporting. Cares about OpEx, risk reduction, audit outcomes, compliance.
- **Champion:** SAS platform owner / technical manager drowning in ad-hoc data-load requests.
- **End users:** business users (Editors) submitting data; data owners (Approvers) signing off; Auditors reviewing history. Roles: Viewer, Editor, Approver, Auditor, Administrator.
- **Gatekeepers:** IT security (satisfied by: on-prem, no external calls, uses existing SASLogon auth, row & column level security, end users never get direct write access to target tables).
**Audience tone (from the LinkedIn guide):** assume competence - don't explain ETL or data warehouses. Speak to their pains: reporting deadlines, audit trails, hand-cranked ETL, EUC risk, OpEx. Professional and direct; no hype.
## Value proposition / key messages
- **Zero code** - adding a new editable table is configuration, done in the tool itself. Column types, lengths, dropdowns are derived from the target table at runtime.
- **Review / approve workflow** - nothing is applied without 1+ approvals; approvers see only the diff.
- **Full audit history** - who, when, why, what; original uploaded files retained.
- **Secure** - existing SAS authentication; RLS and CLS enforced server-side; no end-user write access to targets.
- **Real-time** - intra-day concurrent updates via lock table and queuing; loads abort if the table changed since the diff.
- **Any database** - via SAS libname engines; supports retained keys, composite keys, SCD2 (TXTEMPORAL) and full BITEMPORAL history.
- **Validation at point of entry** - HARDREGEX/SOFTREGEX, dropdowns, min/max, NOT NULL, plus hook scripts to run arbitrary SAS programs before/after edit or approve.
- **Saves vs alternatives** - years of dev/test vs bespoke build; no black box.
## Packaging & pricing
Four packages, all with **unlimited users** (since v6), all flavours of SAS (Viya / EBI / SASjs Server):
| Package | Price | Shape |
| --- | --- | --- |
| Community | FREE | 35 tables/library, 15 view rows, 5 upload rows, 1 viewbox, best-endeavours support |
| Bronze | 12k EUR pa | 1 production env, 100 tables/library, 100 rows, 8hr SLA, SASjs support |
| Silver | 21k EUR pa | 2 production envs, unlimited tables/rows, 6 viewboxes, in-house SAS app support |
| Gold | Contact us | Unlimited envs, 4hr SLA, developer + consulting days, **perpetual licence option** |
- Add-on: Developer Days (new product features) at 600 EUR/day for all plans; consulting rates at sasapps.io/pricing.
- Pricing goals: Transparent, Simple, Fair. Full public price list on the site - a differentiator; use it.
- Support: tickets via support.4gl.io or support@datacontroller.io, severity levels 1-4, SLAs per package, escalation by video call between managers.
## Sales motion / execution strategy
1. **Land with the free tier.** The Community package and the evaluation SPK (single-file import, ~minutes to install on SAS 9) make trying the product nearly frictionless. Since v6.1 the source is also available for anyone to build and evaluate. Push evaluation hard - it is the top of funnel. The eval version is deliberately not production-grade (BASE-engine config tables, table-lock risk, unlicensed interface) - the natural upgrade path is built in.
2. **Discovery call as the primary CTA.** Marketing (LinkedIn, blog, feed) always ends with "book a Discovery Call". Content strategy: hook with the pain (EUC risk, ETL sprawl, audit pain), not the product.
3. **Expand from one use case.** Typical wedge: steering tables / mapping tables / parameter tables in a reporting team. Expansion levers: more tables, more environments, more business units, API-driven submissions (API Explorer since v6).
4. **Sell the business case, not features.** Use the ROI/Payback blog post (five cost areas + calculator) so the champion can build an internal business case. Quantify dev hours, batch incidents, audit effort.
5. **Partner channel.** Resellers exist (e.g. SaasNow in the Netherlands); partner-purchased deals may have partner first-line support.
6. **Roadmap as a sales tool.** The public roadmap is customer-driven - "features requested by customers get built" - and Gold includes Developer Days. Position the product as one that listens.
7. **Proof points.** Named customer stories (AFA, Allianz, Siemens Healthineers, DER Touristik), SAS 2021 Excellence in Innovation Award finalist.
## Getting it installed (technical ease-of-adoption)
- Deploys entirely inside the customer's existing SAS platform - no new infrastructure, no external assets or network calls at runtime (passes strict security reviews).
- SAS 9 EBI: SPK import via SAS Management Console / DI Studio; run the configurator stored process; done. Requirements documented at docs.datacontroller.io (`dci-requirements`, `dci-deploysas9`, `dci-stpinstance`).
- Viya and SASjs Server deployments also documented (`deploy-viya`).
- Instillation/deployment support is in-scope for all paid support plans.
## Licensing
- Annual subscription per package (Bronze/Silver/Gold); perpetual licence available at Gold.
- Community tier is free; the evaluation build is governed by an evaluation agreement (Bowe IO Ltd) - evaluation/non-production use only, no redistribution, no reverse engineering, "as is".
- Embedded Handsontable requires a licence key for commercial use - handled as part of paid deployments.
## Objection handling (from the FAQ)
- **"Can it change our data model?"** - No; it updates data, not structure. Positioned as a safety feature.
- **"Our preferred database?"** - Yes, any engine SAS can reach (incl. Snowflake, Redshift support announcements).
- **"Retained / surrogate keys?"** - Supported, along with composite primary keys, SCD2 and bitemporal loads.
- **"Security review will block it."** - On-prem, offline, existing SASLogon, RLS/CLS, no direct target access, no metadata modification after install.
- **"We could build this."** - Yes, at the cost of years of dev/test plus an unmaintainable black box; DC is proven, supported, documented, and source-available for evaluation.
## Copy rules when writing sales-adjacent content
- Regular dashes, not em-dashes. No hard-wrapped Markdown.
- Use product glossary terms exactly (submission, staging, target table, load type, etc. - see `dc/CONTEXT.md`).
- ® on first mention of SAS®.
- CTA conventions per channel: site content may link directly; LinkedIn puts links in the first comment (see `.agents/docs/linkedin.md`).
+88
View File
@@ -0,0 +1,88 @@
---
name: add-feed-post
description: Add a new Feed post (short announcement / social-style update) to the Data Controller marketing site. Use when the user wants to publish a Feed post, add a /feed/ entry, or create a short announcement that also goes out via RSS.
---
# Add a Feed post
The **Feed** section (`/feed/`) is for short announcements and social-media style updates - things that don't warrant a full `content/blog/` article but are still worth publishing and syndicating via RSS. Feed posts work exactly like blog posts (one markdown file per post, in its own directory) but live under `content/feed/` and publish under `/feed/`.
Both Blog and Feed share a single combined RSS feed at `/rss.xml` (via `gatsby-plugin-feed` in `gatsby-config.js`), so a new Feed post appears in RSS automatically on the next build - no extra config.
## Steps
### 1. Create the post directory
Create a folder under `content/feed/`, named after the post slug (this becomes the URL):
```
content/feed/my-new-announcement/index.md
```
Pick a short, hyphenated, lowercase slug.
### 2. Write the front matter + content
```md
---
title: 'My New Announcement'
description: A one or two sentence summary used for SEO and RSS.
date: '2024-06-01 09:00:00'
author: 'Your Name'
authorLink: https://www.linkedin.com/in/yourprofile/
tags:
- Announcements
---
Your announcement content goes here, written in regular Markdown.
```
Front matter fields:
| Field | Required | Notes |
| ------------- | -------- | --------------------------------------------------------------------- |
| `title` | Yes | Used on the page, in the sidebar and in the RSS item. |
| `description` | Yes | Short summary for SEO meta tags and the RSS item description. |
| `date` | Yes | Format `'YYYY-MM-DD HH:MM:SS'`. Controls sort order and the archive. |
| `author` | Yes | Displayed under the post title. |
| `authorLink` | No | If set, the author name links out (e.g. to a LinkedIn profile). |
| `tags` | Yes | One or more tags. Drives the sidebar category list and RSS category. |
| `previewImg` | No | Relative path to an image in the same folder (e.g. `'./cover.png'`). The template renders this image on the post page automatically - do NOT also embed the same image in the markdown body. |
Use regular dashes (`-`) in content, not em-dashes.
If the post is adapted from source copy the user provided (e.g. a LinkedIn post), paste the original text into an HTML comment (verbatim, except strip the literal `hashtag` prefix that LinkedIn sometimes adds before `#tags`, e.g. `hashtag#sas` -> `#sas`) (`<!-- ... -->`) at the bottom of the markdown file, labelled (e.g. `Source LinkedIn post:`). This preserves the original - including hashtags and checkmarks that get dropped/adapted in the site version - for future reuse.
**Important:** when editing an existing feed post that has a `Source LinkedIn post:` comment, ALWAYS update the LinkedIn version too, so the overall message stays consistent (same points, same claims, same ordering of any lists). Wording does not need to be identical, and formatting will differ: the article uses Markdown, while LinkedIn is plain text with `✓`-style bullets, hashtags, and a comment-based CTA (see `.agents/docs/linkedin.md`).
Every new feed post should include an image (a `previewImg` cover) - and its full image-generation prompt must be recorded in the same HTML comment block, labelled (e.g. `Image prompt:`), so it can be tweaked and regenerated later. If the user has not provided or asked for an image, propose one (theme, style and a draft prompt) and add the `Image prompt:` comment even if the image itself is not yet generated.
### 3. Add images (optional)
If you set `previewImg` or reference images in the body, place the image files in the same post directory and reference them with a relative path.
### 4. Build / preview
```shell
npm run develop
```
The post will be available at `/my-new-announcement/` and will appear on the `/feed/` listing page, in the site search index, and in `/rss.xml` alongside blog posts.
## How Feed differs from Blog
| Aspect | Blog | Feed |
| ----------------- | ----------------------------- | --------------------------------- |
| Content directory | `content/blog/` | `content/feed/` |
| Listing page | `/blog/` | `/feed/` |
| Year archive | `/{year}/` | `/feed/{year}/` |
| Category page | `/category/{tag}/` | `/feed/category/{tag}/` |
| Template | `src/templates/blog-post.tsx`, `src/templates/blog-list.tsx` | `src/templates/feed-post.tsx`, `src/templates/feed-list.tsx` |
| RSS | Included in `/rss.xml` | Included in `/rss.xml` |
Feed archive/category routes are prefixed with `/feed/` to avoid colliding with the equivalent Blog routes, since both are rendered by the shared `src/templates/sidebar.tsx` component (via its `basePath` prop).
## Notes
- To add a **blog** post instead, follow the same steps but use `content/blog/` and the `/blog/` routes.
- The canonical human-readable version of this guide is `docs/adding-feed-posts.md`.
-7
View File
@@ -1,7 +0,0 @@
---
name: grill-with-docs
description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
disable-model-invocation: true
---
Run a `/grilling` session, using the `/domain-modeling` skill.
@@ -1,5 +0,0 @@
interface:
display_name: "Grill with Docs"
short_description: "Grill a design and write its docs"
policy:
allow_implicit_invocation: false
@@ -1,116 +0,0 @@
---
name: setup-matt-pocock-skills
description: Configure this repo for the engineering skills — set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
disable-model-invocation: true
---
# Setup Matt Pocock's Skills
Scaffold the per-repo configuration that the engineering skills assume:
- **Issue tracker** — where issues live (GitHub by default; local markdown is also supported out of the box)
- **Triage labels** — the strings used for the five canonical triage roles
- **Domain docs** — where `CONTEXT.md` and ADRs live, and the consumer rules for reading them
This is a prompt-driven skill, not a deterministic script. Explore, present what you found, confirm with the user, then write.
## Process
### 1. Explore
Look at the current repo to understand its starting state. Read whatever exists; don't assume:
- `git remote -v` and `.git/config` — is this a GitHub repo? Which one?
- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either?
- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root
- `docs/adr/` and any `src/*/docs/adr/` directories
- `docs/agents/` — does this skill's prior output already exist?
- `.scratch/` — sign that a local-markdown issue tracker convention is already in use
- Is the `triage` skill installed? (a `triage` skill folder alongside this one, or `triage` in your available skills.) This decides whether Section B runs at all.
- Monorepo signals — a `pnpm-workspace.yaml`, a `workspaces` field in `package.json`, or a populated `packages/*` with its own `src/`. Present only in a genuinely large multi-package repo; their absence means single-context, which is almost every repo.
### 2. Present findings and ask
Summarise what's present and what's missing. Then take the sections in order — one section, one answer, then the next.
Lead each section with the recommended answer so the user can accept it in a word. Give a one-line explainer only when the choice genuinely branches; skip the section entirely when exploration already settled it (Section B when `triage` isn't installed, Section C when there's no monorepo).
**Section A — Issue tracker.**
> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-tickets`, `triage`, `to-spec`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo.
Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer:
- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI)
- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI)
- **Local markdown** — issues live as files under `.scratch/<feature>/` in this repo (good for solo projects or repos without a remote)
- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose
Record the choice in `docs/agents/issue-tracker.md`. The GitHub and GitLab templates carry a "PRs as a request surface" flag, defaulted **off** — leave it off and don't raise it; a user who wants external PRs in the triage queue can flip the flag in the file later.
**Section B — Triage label vocabulary.** Skip this section entirely if the `triage` skill isn't installed (exploration told you) — an uninstalled skill needs no labels.
If it is installed, ask exactly one question:
> Do you want to keep the default triage labels? (recommended: **yes**)
The defaults are the five canonical roles, each label string equal to its name: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. On **yes**, write them as-is. Only if the user says no — usually because their tracker already uses other names (e.g. `bug:triage` for `needs-triage`) — collect the overrides so `triage` applies existing labels instead of creating duplicates.
**Section C — Domain docs.** Default to **single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. This fits almost every repo; write it without asking.
Offer **multi-context** — a root `CONTEXT-MAP.md` pointing to per-context `CONTEXT.md` files — only when exploration found monorepo signals. Then confirm which layout they want.
### 3. Confirm and edit
Show the user a draft of:
- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules)
- The contents of `docs/agents/issue-tracker.md`, `docs/agents/domain.md`, and `docs/agents/triage-labels.md` (the last only when `triage` is installed)
Let them edit before writing.
### 4. Write
**Pick the file to edit:**
- If `CLAUDE.md` exists, edit it.
- Else if `AGENTS.md` exists, edit it.
- If neither exists, ask the user which one to create — don't pick for them.
Never create `AGENTS.md` when `CLAUDE.md` already exists (or vice versa) — always edit the one that's already there.
If an `## Agent skills` block already exists in the chosen file, update its contents in-place rather than appending a duplicate. Don't overwrite user edits to the surrounding sections.
The block:
```markdown
## Agent skills
### Issue tracker
[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`.
### Triage labels
[one-line summary of the label vocabulary]. See `docs/agents/triage-labels.md`.
### Domain docs
[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`.
```
Include the `### Triage labels` sub-block, and write `docs/agents/triage-labels.md`, only when `triage` is installed and Section B ran. When it isn't, both are omitted.
Then write the docs files using the seed templates in this skill folder as a starting point:
- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker
- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker
- [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker
- [triage-labels.md](./triage-labels.md) — label mapping (only if `triage` is installed)
- [domain.md](./domain.md) — domain doc consumer rules + layout
For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description.
### 5. Done
Tell the user the setup is complete and which engineering skills will now read from these files. Mention they can edit `docs/agents/*.md` directly later — re-running this skill is only necessary if they want to switch issue trackers or restart from scratch.
@@ -1,5 +0,0 @@
interface:
display_name: "Setup Matt Pocock Skills"
short_description: "Configure a repo for the skills"
policy:
allow_implicit_invocation: false
@@ -1,51 +0,0 @@
# Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Before exploring, read these
- **`CONTEXT.md`** at the repo root, or
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
## File structure
Single-context repo (most repos):
```
/
├── CONTEXT.md
├── docs/adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
```
/
├── CONTEXT-MAP.md
├── docs/adr/ ← system-wide decisions
└── src/
├── ordering/
│ ├── CONTEXT.md
│ └── docs/adr/ ← context-specific decisions
└── billing/
├── CONTEXT.md
└── docs/adr/
```
## Use the glossary's vocabulary
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
@@ -1,45 +0,0 @@
# Issue tracker: GitHub
Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
## Conventions
- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
- **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
- **Comment on an issue**: `gh issue comment <number> --body "..."`
- **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
- **Close**: `gh issue close <number> --comment "..."`
Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone.
## Pull requests as a triage surface
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
- **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`).
- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`.
## When a skill says "publish to the issue tracker"
Create a GitHub issue.
## When a skill says "fetch the relevant ticket"
Run `gh issue view <number> --comments`.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
- **Claim**: `gh issue edit <n> --add-assignee @me` — the session's first write.
- **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
@@ -1,46 +0,0 @@
# Issue tracker: GitLab
Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations.
## Conventions
- **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor.
- **Read an issue**: `glab issue view <number> --comments`. Use `-F json` for machine-readable output.
- **List issues**: `glab issue list -F json` with appropriate `--label` filters.
- **Comment on an issue**: `glab issue note <number> --message "..."`. GitLab calls comments "notes".
- **Apply / remove labels**: `glab issue update <number> --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag.
- **Close**: `glab issue close <number>`. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note <number> --message "..."`, then close.
- **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`.
Infer the repo from `git remote -v` — `glab` does this automatically when run inside a clone.
## Merge requests as a triage surface
**MRs as a request surface: no.** _(Set to `yes` if this repo treats external merge requests as feature requests; `/triage` reads this flag.)_
When set to `yes`, MRs run through the same labels and states as issues, using the `glab mr` equivalents:
- **Read an MR**: `glab mr view <number> --comments` and `glab mr diff <number>` for the diff.
- **List external MRs for triage**: `glab mr list -F json`, then keep only MRs whose author is not a project member/owner (a contributor's MR, not a maintainer's in-flight work).
- **Comment / label / close**: `glab mr note`, `glab mr update --label`/`--unlabel`, `glab mr close`.
Unlike GitHub, GitLab numbers issues and MRs separately, so `#42` is unambiguous once you know which surface the maintainer means.
## When a skill says "publish to the issue tracker"
Create a GitLab issue.
## When a skill says "fetch the relevant ticket"
Run `glab issue view <number> --comments`.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `glab issue create --label wayfinder:map`. (On GitLab tiers with native epics, an epic may hold the map instead; a labelled issue works everywhere.)
- **Child ticket**: an issue carrying `Part of #<map>` at the top of its description and labels `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
- **Blocking**: GitLab's **native blocking link** — the canonical, UI-visible representation. Add it with the `/blocked_by #<n>` quick action, posted as a note (`glab issue note <child> --message "/blocked_by #<blocker>"`). Native blocking links are a Premium/Ultimate feature; on the free tier (or where unavailable) fall back to a `Blocked by: #<n>, #<n>` line at the top of the description. A ticket is unblocked when every blocker is closed.
- **Frontier query**: `glab issue list -F json` scoped to the map's children, drop any with an open blocker — a native `blocked_by` link to an open issue (`glab api projects/:id/issues/:iid/links`), or an open issue in the `Blocked by` line — or an assignee; first in map order wins.
- **Claim**: `glab issue update <n> --assignee @me` — the session's first write.
- **Resolve**: `glab issue note <n> --message "<answer>"`, then `glab issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
@@ -1,30 +0,0 @@
# Issue tracker: Local Markdown
Issues and specs (you may know a spec as a PRD) for this repo live as markdown files in `.scratch/`.
## Conventions
- One feature per directory: `.scratch/<feature-slug>/`
- The spec is `.scratch/<feature-slug>/spec.md`
- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` — never a single combined tickets file
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
## When a skill says "publish to the issue tracker"
Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
## When a skill says "fetch the relevant ticket"
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a file with one **child** file per ticket.
- **Map**: `.scratch/<effort>/map.md` — the Notes / Decisions-so-far / Fog body.
- **Child ticket**: `.scratch/<effort>/issues/NN-<slug>.md`, numbered from `01`, with the question in the body. A `Type:` line records the ticket type (`research`/`prototype`/`grilling`/`task`); a `Status:` line records `claimed`/`resolved`.
- **Blocking**: a `Blocked by: NN, NN` line near the top. A ticket is unblocked when every file it lists is `resolved`.
- **Frontier**: scan `.scratch/<effort>/issues/` for files that are open, unblocked, and unclaimed; first by number wins.
- **Claim**: set `Status: claimed` and save before any work.
- **Resolve**: append the answer under an `## Answer` heading, set `Status: resolved`, then append a context pointer (gist + link) to the map's Decisions-so-far in `map.md`.
@@ -1,15 +0,0 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
Edit the right-hand column to match whatever vocabulary you actually use.
-1
View File
@@ -14,7 +14,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
+24 -3
View File
@@ -1,12 +1,33 @@
## Agent skills
## Related repositories
This is the Data Controller **marketing site, blog and feed** (Gatsby). It has two sibling repos, usually checked out side by side:
- **`dc`** - the product source (Angular client + SAS backend). Behaviour of the product itself is defined there; see its `CONTEXT.md` for the domain glossary.
- **`docs.datacontroller.io`** - the user-facing product documentation (MkDocs), published at docs.datacontroller.io.
When writing marketing/blog/feed copy about a feature, treat `docs.datacontroller.io` and `dc` as the source of truth for how the product actually behaves.
## Content structure
- **Blog** (`content/blog/`) - long-form articles, published at `/blog/`.
- **Feed** (`content/feed/`) - short announcements, published at `/feed/`.
- Both are combined into a single RSS feed at `/rss.xml`.
- To add a feed post, use the `add-feed-post` skill (`.agents/skills/add-feed-post/SKILL.md`).
## Agent files
All agent-facing material lives under `.agents/`:
- `.agents/skills/` - pi-discovered skills (e.g. `add-feed-post`).
- `.agents/docs/` - reference docs consumed by skills and these instructions. Notably `sales.md` (product positioning, pain points, audience, pricing and sales motion) - consult it when writing marketing, blog, feed or sales copy.
### Issue tracker
Issues live as markdown files under `.scratch/`. See `docs/agents/issue-tracker.md`.
Issues live as markdown files under `.scratch/`. See `.agents/docs/issue-tracker.md`.
### Domain docs
Single-context layout - `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.
Single-context layout - `CONTEXT.md` at the repo root + ADRs in `.agents/adr/`. See `.agents/docs/domain.md`.
### Writing style
+30
View File
@@ -0,0 +1,30 @@
# Context: datacontroller.io (marketing site)
The public marketing website for Data Controller for SAS®, built with **Gatsby** (React + TypeScript, styled-components). It hosts the product's marketing pages, blog and feed. It is separate from the product itself (`dc`) and the product documentation (`docs.datacontroller.io`).
## What the product is
Data Controller for SAS® is a web application that lets users safely add, modify and delete data in SAS datasets and databases, with an edit-stage-approve workflow and full change history. It runs on SAS Viya, SAS 9 EBI and SASjs Server. For the product's own domain vocabulary (roles, load types, MPE control tables, validations, security), see `dc/CONTEXT.md`; for user-facing feature docs, see the `docs.datacontroller.io` repo. Treat those as the source of truth when writing copy about how the product behaves.
## Layout
- `content/blog/` - long-form articles (published at `/blog/`).
- `content/feed/` - short announcements (published at `/feed/`). See the `add-feed-post` skill.
- `src/pages/` - top-level pages (`index`, `about`, `contact`, `faq`, plus the markdown-driven `{MarkdownRemark.frontmatter__slug}.tsx`).
- `src/markdown-pages/` - standalone markdown pages (e.g. `pricing.md`).
- `src/templates/` - list/post/sidebar templates for blog and feed.
- `src/components/` - shared React components (layout, navibar, footer, hero, seo).
- `static/` - static assets served as-is; `gatsby-*.js` - Gatsby config (feed, node APIs, etc.).
## Conventions
- Use regular dashes (`-`) in content, not em-dashes.
- Blog and feed share a single combined RSS feed at `/rss.xml` (via `gatsby-plugin-feed`); new posts appear automatically on the next build.
- Lint with `npm run lint` (prettier) and typecheck with `npm run typecheck` before considering front-end changes done.
- Preview locally with `npm run develop`.
## Agent files
All agent-facing material lives under `.agents/`: skills in `.agents/skills/` (e.g. `add-feed-post`), and reference docs in `.agents/docs/` (`domain.md`, `issue-tracker.md`).
See `AGENTS.md` for agent skills, writing rules, and where these files live.
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,199 @@
---
title: "v7.13 Release: Formulas & Regex"
description: Data Controller 7.13 wires a spreadsheet-grade formula engine into the editor grid, completing the point-of-entry validation story that began with 7.12's regex rules. Both are pure MPE_VALIDATIONS configuration - no code, no deployment.
date: '2026-09-03 12:00:00'
author: 'Allan Bowe'
authorLink: https://www.linkedin.com/in/allanbowe/
previewImg: './v713_cover.png'
tags:
- Releases
- Data Controller
---
# v7.13: Formulas & Regex
We stopped writing release-specific blog posts after v6.1 - the automated, public release system made them redundant for routine version bumps. But every so often a release lands that changes what the product can actually _do_, and v7.13 is one of them. Together with 7.12 it completes a piece of the [roadmap](https://docs.datacontroller.io/roadmap/) we have been chipping away at for a while: **frontend formulae and regex rules**, both configurable purely in `MPE_VALIDATIONS`.
If you configure data entry in Data Controller, this post is a practical guide: how the rules work, how to set them up, and the process flow from config to grid. All the screenshots below are captured from a live editor session against the demo tables, so what you see is what shipped.
## Why this matters
Data Controller's job is to let business users change data safely. A big part of "safely" is catching problems at the point of entry - rather than after an approval, in a batch log, or (worst case) in a report. The [validations](https://docs.datacontroller.io/dcc-validations/) framework already covered length, type, nullability, primary keys, ranges, casing and dropdowns. Two things were missing:
* **Computed values.** Plenty of tables have columns that are derived from other columns - a revenue column that is price times volume, a stamp column that records who last touched a row. Until now the options were a backend hook script (real SAS code to write, test and deploy) or just letting users type anything.
* **Pattern enforcement.** A dropdown is overkill when all you need is "this value looks like an email address" or "this postcode is well-formed". You want the _shape_ of the value checked as typed - and sometimes you want a hard block, sometimes just a gentle warning.
7.12 delivered regex rules (`HARDREGEX` / `SOFTREGEX`). 7.13 delivers formulas (`HARDFORMULA` / `SOFTFORMULA`). Both are rows in a config table. That is the whole feature.
## The process flow: from MPE_VALIDATIONS to the grid
Every configurable rule in Data Controller follows the same pipeline, and the new rules plug straight into it:
1. **Configure.** `MPE_VALIDATIONS` is itself a Data Controller table - open it from the navigation tree like any other, and add a row with `BASE_LIB`, `BASE_DS` and `BASE_COL` pointing at the column you want to govern, `RULE_TYPE` set to the new rule, `RULE_VALUE` holding the formula or pattern, and `RULE_ACTIVE=1`. Submit, approve, done - it's a config change, not a code release. The [MPE_VALIDATIONS table guide](https://docs.datacontroller.io/tables/mpe_validations/) has the full column reference.
2. **Serve.** When a user opens the editor, the [`editors/getdata`](https://code.datacontroller.io/getdata_8sas.html) service extracts the active rules for that table - it filters `MPE_VALIDATIONS` on library, table and `RULE_ACTIVE=1` - and returns them in the `dqrules` object of the response, alongside the table data, schema, and the schema-derived `NOTNULL` constraints.
3. **Apply.** The frontend wires each rule into the Handsontable grid: formula rules are computed live by [HyperFormula](https://hyperformula.handsontable.com/) (the calculation engine behind Handsontable itself), regex rules are evaluated in the browser with the JavaScript regex engine.
4. **Block or warn.** On submit, the standard [cell validation](https://docs.datacontroller.io/dcc-validations/) cycle runs: `HARD` rules block the submission if violated, `SOFT` rules warn but allow.
Here are the new rule types as they appear in the config - one row per rule, `RULE_VALUE` holding the formula or the pattern:
![](./mpe_validations_rules.png)
For regex there is also a config-time guard, and it's a neat piece of dogfooding: because the rule itself lives in a table, saving an edit to `MPE_VALIDATIONS` through Data Controller runs the [post-edit hook](https://code.datacontroller.io/mpe__validations__postedit_8sas.html), which passes every `HARDREGEX` / `SOFTREGEX` `RULE_VALUE` through `PRXPARSE` and rejects the edit if the pattern is invalid - listing the offending columns. A typo in a pattern is caught the moment you save the rule, not the first time a user hits it.
## Regex rules (HARDREGEX / SOFTREGEX, v7.12)
Regex rules validate cell values against a SAS (Perl-style) regular expression. You provide the pattern in `RULE_VALUE`; whether it blocks or warns depends on the rule type:
* **HARDREGEX** - the value **must** match the pattern. If it doesn't, the cell is highlighted red and submission is blocked.
* **SOFTREGEX** - a non-matching value is highlighted yellow as a warning. The user can still submit - it's a nudge, not a block.
### Setting one up
Say `SOME_CHAR` in the demo table must contain either "the" or "data" (case-insensitive). That's one insert:
```sas
insert into &lib..MPE_VALIDATIONS set
tx_from=0
,base_lib="&lib"
,base_ds="MPE_X_TEST"
,base_col="SOME_CHAR"
,rule_type='HARDREGEX'
,rule_value='/the|data/i'
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
```
That's a real row from the demo data, by the way - the shipped `MPE_X_TEST` table carries sample `HARDREGEX` and `SOFTREGEX` rules so you can try both without touching your own config.
In the editor it looks like this - an invalid email blocked red by `HARDREGEX`, an invalid postcode warned yellow by `SOFTREGEX`, and a value that passes:
![](./regex_demo.png)
Note the third column in that screenshot, `REGEX_BOTH_COL`. It carries _both_ a `HARDREGEX` and a `SOFTREGEX` rule - and shows neither warning. That's the precedence rule in action: only one regex is ever applied per column, and if both exist the `SOFTREGEX` is ignored entirely, so the column behaves exactly as if it were `HARDREGEX`-only.
### What to know before writing patterns
The full list of gotchas is in the [regex rules documentation](https://docs.datacontroller.io/dcc-validations/#regex-rules); the ones that matter most in practice:
* **Use the PRX delimiter form.** Patterns are authored exactly as [PRXPARSE](https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/lefunctionsref/p0s9ilagexmjl8n1u7e1t1jfnzlk.htm) accepts them: `/pattern/flags`, e.g. `/^\d+$/` for "integers only", `/the|data/i` for a case-insensitive contains. The config-time `PRXPARSE` check enforces this - a bare pattern without delimiters will be rejected when you save the rule. (The frontend tolerates bare patterns for backwards compatibility, but don't author new rules that way.)
* **Anchor your own patterns.** The pattern is used **as authored** - it is not auto-anchored. `/the|data/i` matches "the" _anywhere_ in the value. If you want the whole value to match, include `^` and `$` yourself.
* **Stick to the common subset.** The pattern is evaluated in the browser with the JavaScript regex engine, which shares SAS PRX's core syntax (character classes, quantifiers, groups, alternation, `^`/`$` anchors, `\d \w \s` and friends). Three unambiguous Perl-isms are translated automatically - a leading `(?i)` modifier, `\Q...\E` literal sequences, and `\A`/`\z` absolute anchors. But Perl-only constructs such as possessive quantifiers (`a++`) and atomic groups (`(?>...)`) pass the SAS-side check and then silently do nothing in the frontend - so just don't use them.
* **Blank is exempt.** Blank values skip pattern matching on any column type (use the `NOTNULL` rule if you also need populated values). On numeric columns the plain SAS missing (`.`) is also exempt - but special missings (`.A`-`.Z`, `._`) are not: they're deliberately-set values, so your pattern needs to accommodate them.
* **One regex per column.** As above - `HARDREGEX` wins when both are present, and the column-header info dropdown shows only the rule that is actually applied.
* **Length limit.** `RULE_VALUE` is 128 characters, which constrains very long patterns.
* **Deleted rows are exempt.** Cells in rows marked for deletion are not validated or warned (except primary key columns, which still are).
### Where regex beats a dropdown
For currency codes, country codes, account number formats, email shapes, or "must be an integer" - a regex is one row of config versus a 1000-value `HARDSELECT` dropdown. And unlike a dropdown, a regex catches _paste_ operations too, not just typed values.
## Formula rules (HARDFORMULA / SOFTFORMULA, v7.13)
Formula rules make a column compute itself from other columns in the same row - like a spreadsheet formula, but the formula lives in config and applies to every row. When a user opens the editor, the formula is evaluated live and the result is shown in each cell. Change an input, and every dependent cell recalculates on the spot.
### Writing a formula
Formulas use **column names, not cell references** - there is no need to know the grid layout. If you have `A_COL` and `B_COL`, a `FORMULA_HARD_COL` rule is simply:
```
=A_COL * B_COL
```
Each row calculates its own result: row 1's values, row 2's values, and so on. Under the hood each column name is translated to a row-relative cell reference (text inside quotes is left untouched) and handed to HyperFormula - so you get the full spreadsheet function library, `IF`, `SUM`, `ROUND`, `CONCAT` and friends, without writing any code.
The one syntax rule: **each column name must be surrounded by spaces**. `=MATCH( PRICE )` resolves the column reference; `=MATCH(PRICE)` does not - without the surrounding blanks the token isn't recognised as a column reference, and the formula errors rather than using the column's value. The spaces stop column names clashing with function names.
### HARD vs SOFT formulas
* **HARDFORMULA** - the column is read-only. The formula result is always shown and submitted; the user cannot change it. Think calculated amounts, or a `PROCESSED_BY` column.
* **SOFTFORMULA** - the cell shows the formula result, but the user can type a different value if the computed one is wrong. Their value is submitted instead. Useful for derived defaults where the business occasionally needs to override.
### Special values
Formulas can reference three runtime values, resolved when the formula is evaluated:
* `DC.ROW_STATUS` - the row's current state: `M` (Modified), `A` (Added), `D` (Deleted), or `U` (Unchanged). A newly-added row is `A` from the moment it is created - there is no transient state before that.
* `DC.USER_NAME` - the logged-in user id.
* `DC.ORIG_VALUE` - the original cell value before the current edit.
Our demo formula table puts them all to work. There's a row-status column (`=DC.ROW_STATUS`), a user column (`=DC.USER_NAME`), and a `CHANGE_SUMMARY_COL` whose rule reads like a proper audit sentence:
```
=IF( DC.ROW_STATUS ="U","unedited",
DC.USER_NAME &" changed from "& DC.ORIG_VALUE )
```
Here it is live in the editor. We edited `B_COL` on the second row from 10 to 25 - and the whole row reacted: `FORMULA_HARD_COL` recomputed to 50 (`A_COL * B_COL`), `FORMULA_SOFT_COL` recomputed to 27 (`A_COL + B_COL`), and the row status flipped from `U` to `M` - all instantly, all without touching a single line of SAS:
![](./formula_demo.png)
And here's the payoff of those `DC.*` references - the audit columns from the same table, same edit. `ROW_STATUS_COL` flipped to `M`, and the change summary resolved the `IF` formula above into a sentence - "sasdemo changed from orig-2":
![](./formula_audit_demo.png)
Because `DC.ROW_STATUS` is a live reference, it updates as the user works: edit a cell and the stamp flips to your user id; cancel the edit and it reverts.
### Editor behaviour worth knowing
* When you paste a formula into the grid, column names are automatically translated so the formula works in its new position.
* A cell that is overwritten by a formula is flagged (with the original value retained) so you can revert it.
* Formula-looking values pasted from Excel are treated as plain data (not evaluated), unless you explicitly choose "Apply as formula" - a deliberate safety measure so a spreadsheet's internal formulas don't leak into your data as live rules.
* When submitted, it's the formula's computed value that is sent to the backend, never the raw formula text (a primary key column always resolves its live formula immediately to the computed value so the submission keys are correct).
## How it works under the hood (briefly)
Two moving parts, and the boundary between them explains most of the gotchas above.
**Serving the rules.** [`getdata`](https://code.datacontroller.io/getdata_8sas.html) extracts the active `MPE_VALIDATIONS` rows for the target table and returns them in `dqrules`, along with the schema-derived `NOTNULL` constraints. Formulas and regex are frontend rules - they are evaluated in the browser, not in SAS - which is why they arrive as `dqrules` rather than as backend hook scripts.
**Evaluating them.** For formulas, the client translates each column name in `RULE_VALUE` to a row-relative cell reference and hands it to HyperFormula (wired into Handsontable's formulas plugin). For regex, the client parses the PRX `/pattern/flags` form, translates the three Perl-isms it can, and constructs a JavaScript `RegExp`. If a pattern still fails in the browser, the editor treats it as always-valid rather than breaking - a failed pattern never blocks a submission it shouldn't.
**Guarding the config.** Because `MPE_VALIDATIONS` is itself a Data Controller table, a [post-edit hook](https://code.datacontroller.io/mpe__validations__postedit_8sas.html) validates new rules: `PRXPARSE` checks every regex `RULE_VALUE`, and the edit is rejected with the offending columns listed. Invalid rules never reach users.
## Also in these releases
Beyond the headline features, the usual spread of hardening and polishing shipped along the way: row-header status cells are now colour-coded (with a `±` symbol for modified rows), CAS support landed for the `REPLACE` load type, Viya deploy diagnostics were improved, and a large tranche of dependency upgrades (Angular 20, Handsontable 18 pinned, sasjs core v5) keeps the audit trail clean. As ever, the full commit-by-commit detail is in the [release notes](https://git.datacontroller.io/dc/dc/releases).
## Upgrading
The frontend changes are included in the 7.13 release assets. The backend additions are **data-only, optional migrations**:
* The **v7.12 migration** adds `HARDREGEX` / `SOFTREGEX` to the `RULE_TYPE` dropdown in `MPE_VALIDATIONS` (and switches the `MPE_SECURITY.LIBREF` validation to a hook that lists all libraries).
* The **v7.13 migration** adds `HARDFORMULA` / `SOFTFORMULA` to the same dropdown.
Both scripts are in [`sas/sasjs/db/migrations/`](https://git.datacontroller.io/dc/dc/src/branch/main/sas/sasjs/db/migrations) in the source repo, and they're worth running even if you don't plan to use the rules immediately - they only add dropdown values to `MPE_SELECTBOX`.
## Try it yourself
The shipped demo data includes the regex rules on `MPE_X_TEST`, so you can see them working without configuring anything: open the demo library, edit `MPE_X_TEST`, and try entering a `SOME_SHORTNUM` between 1 and 5 (blocked red - `HARDREGEX`), a `PRIMARY_KEY_FIELD` with a decimal point (warned yellow - `SOFTREGEX`), or a `SOME_CHAR` without "the" or "data" in it (blocked - `HARDREGEX`).
For formulas, add a rule to one of your own tables - the `REVENUE = PRICE * VOLUME` example above is a two-minute configuration, and the `DC.*` special values make audit-style columns almost free. Full reference in the [validations docs](https://docs.datacontroller.io/dcc-validations/).
As ever - if you'd like to see additional validation types, [get in touch](https://datacontroller.io/pricing). The roadmap is customer-driven, and the validations list keeps growing.
<!-- Source LinkedIn post:
Every SAS team has a spreadsheet that "helps" load data into production.
You know the one. Someone built it years ago, nobody fully understands it, and it writes straight to a table it probably shouldn't.
What if that spreadsheet was wrong BEFORE anyone could hit send?
That's the idea behind Data Controller - a web app for SAS that lets business users change data safely. Every change is checked at the point of entry, goes for approval, and lands in a full audit trail. No new infrastructure - it runs on the SAS you already have.
The latest release adds two things that used to require a developer:
→ Formulas. Type a rule like = PRICE * VOLUME and the revenue column computes itself - spreadsheet-style, in the browser, live.
→ Pattern checks. "Must be a valid email." "Must be an integer." One row of config. Bad values are blocked or flagged before submission - not found in next month's report.
Both are configuration, not code. If you can fill in a table, you can set them up.
We wrote up how it works (with live screenshots of a bad email being caught red-handed):
Link in the comments 👇
#sas #dataquality #endusercomputing #excel #datagovernance
First comment: https://datacontroller.io/v7-13-formulas-and-regex/
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+2 -1
View File
@@ -3,9 +3,10 @@ title: 'Complex Excel Uploads with Excel Maps'
description: Extract scattered, dynamic data from any Excel workbook using configurable Excel Maps - no manual reformatting required.
date: '2026-07-26 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data_controller
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- Announcements
previewImg: './dc-capture-review-approve.jpeg'
---
# Complex Excel Uploads with Excel Maps
@@ -0,0 +1,70 @@
---
title: 'Every SAS® Project Needs to Load Data from Excel'
description: After years of SAS consulting, one pattern emerged - every project needed Excel data loads, and every project built them from scratch. Here is how that journey led to Data Controller.
date: '2026-08-17 19:15:00'
author: 'Allan Bowe'
authorLink: https://www.linkedin.com/in/allanbowe/
tags:
- Announcements
previewImg: './sasprojects.jpeg'
---
After several years of SAS consulting I noticed EVERY SINGLE PROJECT had a need to load data from Excel.
And EVERY SINGLE PROJECT would require dedicated SAS code (or ETL jobs) to read that data, cleanse, append to target, and to run in a secure production environment under batch credentials. This meant a dev/test/accept(+signoff)/prod lifecycle.
This process could literally take weeks.
As projects went by, I crafted a generic data capture tool using VBA, Stored Processes, and a shared network drive to share CSVs with SAS. It was a ZERO CODE solution that worked really well, and allowed ANY table to be configured so that business users could make changes in Excel and upload directly to production (with approval step).
It was a godsend.
Still, it had major drawbacks - users had to self-pull the latest version, and we still had this insecure network drive situation.
By 2020 the codebase was completely re-written as a SASjs Powered Web Application. The shared network drive was gone. The browser always has the latest version. We had SAS Viya compatibility and a growing number of satisfied (paying) customers.
By 2022 we had advanced Data Quality features, support for all flavours of SAS (Base, Meta, Viya), special SAS missing numerics and the ability to update formats in catalogs. Big-name customers in Banking, Insurance, Government, Travel, Pharma and other sectors.
In 2024 we had support for complex excel uploads (extract cells / ranges from anywhere in a workbook), detailed data audit, and a data rollback feature.
In 2026 we have a streamlined and tightened Viya deploy and many more validations - such as regex, and soon, frontend formulae.
The need to load ad-hoc data into SAS isn't going away.
Our mission is to make that process simple, secure, and fast.
To see how easily Data Controller can be deployed in your organisation, book a [Discovery Call](https://datacontroller.io/contact/) - or start today with the free [Community version](https://datacontroller.io/pricing/).
<!--
Source LinkedIn post:
After several years #SAS consulting I noticed EVERY SINGLE PROJECT had a need to load data from Excel.
And EVERY SINGLE PROJECT would require dedicated SAS code (or ETL jobs) to read that data, cleanse, append to target, and to run in a secure production environment under batch credentials. This meant a dev/test/accept(+signoff)/prod lifecycle.
This process could literally take weeks.
As projects went by, I crafted a generic data capture tool using VBA, Stored Processes, and a shared network drive to share CSVs with SAS. It was a ZERO CODE solution that worked really well, and allowed ANY table to be configured so that business users could make changes in Excel and upload directly to production (with approval step).
It was a godsend.
Still, it had major drawbacks - users had to self-pull the latest version, and we still had this insecure network drive situation.
By 2020 the codebase was completely re-written as a SASjs Powered Web Application. The shared network drive was gone. The browser always has the latest version. We had #SASViya compatibility and a growing number of satisfied (paying) customers.
By 2022 we had advanced Data Quality features, support for all flavours of SAS (Base, Meta, Viya), special SAS missing numerics and the ability to update formats in catalogs. Big-name customers in Banking, Insurance, Government, Travel, Pharma and other sectors.
In 2024 we had support for complex excel uploads (extract cells / ranges from anywhere in a workbook), detailed data audit, and a data rollback feature.
In 2026 we have a streamlined and tightened Viya deploy and many more validations - such as regex, and soon, frontend formulae.
The need to load ad-hoc data into SAS isn't going away.
Our mission is to make that process simple, secure, and fast.
To see how easily Data Controller can be deployed in your organisation, book a Discovery Call (link in comments) - or start today with the free Community version.
#datamanagement
Image prompt: A flat-style illustration showing an Excel spreadsheet with an arrow flowing into a secure database server, with a subtle timeline (VBA era to modern web app) in the background, blue and green corporate palette, clean minimal design suitable for a LinkedIn/blog cover image.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

@@ -0,0 +1,67 @@
---
title: 'The 5 Lines of Defence for Business Data Inputs'
description: How Data Controller for SAS® defends data quality for workflows deriving from business inputs - data model, permissions, validation checks, post edit hooks, and approvals.
date: '2026-08-27 09:00:00'
author: 'Allan Bowe'
authorLink: https://www.linkedin.com/in/allanbowe/
tags:
- Announcements
previewImg: './five-lines-of-defence.jpeg'
---
How does Data Controller for SAS® defend data quality for data workflows deriving from business inputs?
**⚔️ 1st Line of Defence - Data Model ⚔️**
The interface will only accept inputs conforming to this model (columns, types, lengths, indexes, constraints etc). The schema determines the behaviour - eg, a date format results in a date picker.
**⚔️ 2nd Line of Defence - Data Permissions ⚔️**
Changes are made using a SAS System Account (eg `sassrv`). This means you can safely DENY write-access to data for business users, preventing un-controlled data ingestion.
**⚔️ 3rd Line of Defence - Validation Checks ⚔️**
Additional validation checks (eg value ranges, specific patterns) can be configured to run at the point of data capture, prior to SAS upload. See the [validation documentation](https://docs.datacontroller.io/dcc-validations/) for details.
**⚔️ 4th Line of Defence - Post Edit Hook ⚔️**
Complex / customer specific validation can be deployed as SAS code, to run after every EDIT (prior to APPROVAL) using a [post edit hook](https://docs.datacontroller.io/dcc-tables/#post_edit_hook). A failure here results in immediate user feedback / change rejection.
**⚔️ 5th Line of Defence - Approval Step(s) ⚔️**
Changes are reviewed with [one or more approvals](https://docs.datacontroller.io/dcc-tables/) BEFORE being applied to the target database. A full audit trail is also maintained.
All functionality is ZERO-CODE, works on SAS Viya / EBI / SASjs Server, and applies to any database you have an ACCESS engine for. There is also a [community edition](https://datacontroller.io/pricing/) - free for unlimited users.
If you'd like to strengthen your own defences - [let's chat](https://datacontroller.io/contact/).
<!--
Source LinkedIn post:
How does Data Controller for SAS® defend #DataQuality for #DataWorkflows deriving from Business Inputs?
⚔️ 1st Line of Defence - Data Model ⚔️
The interface will only accept inputs conforming to this model (columns, types, lengths, indexes, constraints etc). The schema determines the behaviour - eg, a date format results in a date picker.
⚔️ 2nd Line of Defence - Data Permissions ⚔️
Changes are made using a SAS System Account (eg `sassrv`). This means you can safely DENY write-access to data for business users, preventing un-controlled #DataIngestion.
⚔️ 3rd Line of Defence - Validation checks ⚔️
Additional validation checks (eg value ranges, specific patterns) can be configured to run at the point of data capture, prior to #SAS upload. The docs for these are here: https://lnkd.in/djtaGPsr
⚔️ 4th Line of Defence - Post Edit Hook ⚔️
Complex / customer specific validation can be deployed as SAS code, to run after every EDIT (prior to APPROVAL). A failure here results in immediate user feedback / change rejection.
⚔️ 5th Line of Defence - Approval Step(s) ⚔️
Changes are reviewed with one or more approvals BEFORE being applied to the target database. A full audit trail is also maintained.
All functionality is ZERO-CODE, works on #sasViya / EBI / SASjs Server, and applies to any database you have an ACCESS engine for. There is also a Community edition - free for unlimited users.
#datagovernance #sasadmin #saspartners
-->
<!-- Image prompt:
A flat-design illustration in the Data Controller brand style (dark navy background, teal/green and orange accents), composed around a SINGLE CENTRAL focal point so it survives a square crop: a medieval-castle motif reimagined as a data fortress - five concentric defensive walls (rendered as clean glowing ring segments, each a slightly different teal/green shade, subtly numbered 1-5) surrounding a central database cylinder that glows safely at the core. An arrow or data-packet stream approaches from the top, passing checkpoints in each wall: a schema/grid icon (wall 1), a padlock (wall 2), a checklist with ticks (wall 3), a code angle-brackets icon (wall 4), and a stamp/approval tick (wall 5). One red/orange invalid packet is shown bouncing off an outer wall. All key elements within the central square of the frame; outer left and right thirds contain only background texture and glow, safe to crop. Minimal text, no logos. Professional but lightly playful, suitable for a B2B data product site. 16:9 landscape, 1200x627, suitable as a blog/feed cover image.
Generated with: local Routstr node (see blog-dev/skills routstr-image-generation), model gemini-3.1-flash-lite-image, output 1424x736 JPEG, ~31 sats.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

+24
View File
@@ -0,0 +1,24 @@
---
title: 'FSEDIT-Style Paging in the Edit Record Form'
description: Page through your data one record at a time in the Edit Record dialog - a modern take on the classic SAS PROC FSEDIT full-screen editor.
date: '2026-08-09 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- Announcements
previewImg: './example.png'
---
# FSEDIT-Style Paging in the Edit Record Form
Anyone who has worked with SAS for a while will remember `PROC FSEDIT`. Part of the SAS/FSP (Full-Screen Product) family alongside `PROC FSVIEW` and `PROC FSBROWSE`, it was for years the standard way to edit a dataset interactively: one observation per screen, typing directly over values, and commiting straight to the underlying dataset. That focused, one-record-at-a-time view was a simple way to amend data without getting lost in a wide grid - though edits landed immediately, with no staging or approval step in between.
Data Controller's **Edit Record** dialog brings that experience into the browser. Instead of scanning across dozens of columns in the grid, you open a single row as a form - every field labelled and stacked vertically, with dropdowns, validations and formatting applied just as they are in the main editor.
You can **page through your records** without leaving the form. Previous and Next arrows move you one record at a time, and a **Current row** indicator shows exactly where you are in the extract. It's the fastest way to work through a set of rows methodically - reviewing, correcting and moving on - especially for wide tables where horizontal scrolling in the grid slows you down.
<iframe title="Forwards_Backwards_in_Edit_Record_Modal" width="560" height="315" src="https://vid.4gl.io/videos/embed/o8yzHqRqTWhYo6cgNACRkg" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
As always, everything flows through the standard Data Controller workflow: your changes are staged, validated and sent for approval before they reach the target table, with a full audit trail retained.
Administrators who prefer to keep the grid-only experience can disable the dialog with the [`DC_RESTRICT_EDITRECORD`](https://docs.datacontroller.io/dcc-options/#dc_restrict_editrecord) option.
Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

@@ -0,0 +1,68 @@
---
title: 'Some of You Will "Git" This'
description: Pull requests, diffs, hooks, tests and pipelines - for your production data. Data Controller brings the git workflow to master data management in SAS.
date: '2026-08-13 14:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- Announcements
previewImg: './gitflow.png'
---
# Some of You Will "Git" This
If you could make a "pull request" for production data, how might that look?
Perhaps:
- ✅ A [before / after comparison](https://docs.datacontroller.io/dc-userguide/) of the values that had changed
- ✅ [Security](https://docs.datacontroller.io/dcc-security/) (who can submit / approve in each "repo")
- ✅ Only store the changed data (not full copies)
- ✅ A new "diff" when the base table changes
- ✅ Ability to choose [one or more approvers](https://docs.datacontroller.io/dcc-tables/)
- ✅ [HOOK scripts](https://docs.datacontroller.io/dcc-tables/) before / after a commit
- ✅ Tests ([DQ checks](https://docs.datacontroller.io/dcc-validations/)) prior to merge
- ✅ A [pipeline](https://docs.datacontroller.io/dcc-tables/) following a merge
- ✅ [Emails](https://docs.datacontroller.io/emails/) for "status" changes
- ✅ Comments by reviewers
- ✅ Full [history](https://docs.datacontroller.io/dc-userguide/) of changes
Sound familiar? This is exactly how developers have managed *code* for decades - and Data Controller brings the same discipline to *data*. Every edit to a table in SAS® goes through an edit - stage - approve workflow: changes are staged as a diff, checked by validation rules and hook scripts, reviewed and approved by the people you choose, then loaded. The audit trail captures who changed what, when and why - the `git blame` of master data.
If you're ready to "commit" to Data Control, we invite you to "clone" the latest version of [Data Controller for SAS®](https://datacontroller.io) - which has ALL these features - and "push" to your "main" environment for some "master" data management!
<!-- Source LinkedIn post:
Some of you will "git" this..
If you could make a "pull request" for production data, how might that look?
Perhaps:
✅ A before / after comparison of the values that had changed
✅ Security (who can submit / approve in each "repo")
✅ Only store the changed data (not full copies)
✅ A new "diff" when the base table changes
✅ Ability to choose one or more approvers
✅ HOOK scripts before / after a commit
✅ Tests (DQ checks) prior to merge
✅ A pipeline following a merge
✅ Emails for "status" changes
✅ Comments by reviewers
✅ Full history of changes
Sound familiar? This is exactly how developers have managed CODE for decades - and Data Controller brings the same discipline to DATA.
Every edit to a table in Data Controller for SAS® goes through an edit - stage - approve workflow: changes are staged as a diff, checked by validation rules and hook scripts, reviewed and approved by the people you choose, then loaded.
The audit trail captures who changed what, when and why - the "git blame" of master data.
If you're ready to "commit" to Data Control, we invite you to "clone" the latest version of Data Controller for SAS - which has ALL these features - and "push" to your "main" environment for some "master" data management!
Link in the comments below 👇
#dataops #mdm #masterdata #dataquality #sas #sasjs #datagovernance
-->
<!-- Image prompt:
A clean, flat-design illustration in the Data Controller brand style showing a database table flowing through a git-style pull-request workflow: rows of a data table on the left passing through a "stage" step shown as a highlighted before/after diff of changed cells, then an approval checkpoint with a tick and reviewer avatars, then merging into a production database cylinder on the right. Git-flavoured touches: a branch line diagram arcing from "staging" branch into "main", small commit dots along the line. Dark navy background, teal/green accents, orange highlights on the changed cells. Minimal text: "stage -> approve -> merge". 16:9 landscape, suitable as a blog/feed cover image.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

@@ -0,0 +1,73 @@
---
title: 'Horror Stories of Business Data Ingestion'
description: Real-world tales of business data ingestion gone wrong - from broken ETL jobs to network-share "audit trails" and the Access database under the operations desk.
date: '2026-08-15 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
previewImg: './horror.jpeg'
tags:
- Master Data Management
---
# Horror Stories of Business Data Ingestion
> "We spent 6 weeks recruiting a Data Engineer, who built several complex ETL workflows, which promptly broke on his departure thanks to a minor change in the source file"
> "I wrote the SQL update script myself, which was working fine until my holiday cover adjusted the where clause, and wiped half the records. At least, that's what I think happened. We didn't see the discrepancy until the following month."
> "We often need to upload new mapping files and so all the Business Analysts in the Customer Insights team are being trained on the enterprise ETL tool"
> "Our DBA does the manual updates, but he only has 0.5 free days per month allocated to the task - and he will only do it AFTER month end"
> "We have a dedicated data engineering team and they can knock out an ETL job in a few minutes. The problem is that it takes 4-8 hours of documentation and meetings to set the requirements, and 4-8 weeks to deploy the job into production"
> "If you need to change the data values, just double click the table in our .NET application and type it in. Remember to close it afterwards though, else the overnight batch job will fail due to a physical lock. This happens quite a lot - it's safest to just close the application afterwards and re-open it."
> "We keep an audit trail of ALL manual updates. Whoever makes the change will just open the spreadsheet, fill in the necessary records, and save the file with a new version number on the department network share"
> "We don't have any business data ingestion, we only load source data, and it is never modified"
> "We export the data from the web report into Excel. We copy/paste the data into another Excel sheet where we execute some VBscript macros that were developed by a guy who left the company. After that, the updated data are saved as a csv file on a network drive. The file is then picked up by a batch job and loaded into the database. The job is scheduled to run every 15 minutes. The updated records can then be displayed in our reports." - [Xavier Bizoux](https://www.linkedin.com/in/xavierbizoux) ([source](https://www.linkedin.com/feed/update/urn:li:activity:7183406509450547200?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7183406509450547200%2C7184815374624772096%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287184815374624772096%2Curn%3Ali%3Aactivity%3A7183406509450547200%29))
Coupled with:
> "We don't load our data to the DataWarehouse, it's too difficult. We just stick it all in the team ACCESS database on the PC under the operations desk"
What are YOUR horror stories?
<!--
Source LinkedIn post:
Horror Stories of Business Data Ingestion
"We spent 6 weeks recruiting a Data Engineer, who built several complex ETL workflows, which promptly broke on his departure thanks to a minor change in the source file"
"I wrote the SQL update script myself, which was working fine until my holiday cover adjusted the where clause, and wiped half the records. At least, that's what I think happened. We didn't see the discrepancy until the following month."
"We often need to upload new mapping files and so all the Business Analysts in the Customer Insights team are being trained on the enterprise ETL tool"
"Our DBA does the manual updates, but he only has 0.5 free days per month allocated to the task - and he will only do it AFTER month end"
"We have a dedicated data engineering team and they can knock out an ETL job in a few minutes. The problem is that it takes 4-8 hours of documentation and meetings to set the requirements, and 4-8 weeks to deploy the job into production"
"If you need to change the data values, just double click the table in our .NET application and type it in. Remember to close it afterwards though, else the overnight batch job will fail due to a physical lock. This happens quite a lot - it's safest to just close the application afterwards and re-open it."
"We keep an audit trail of ALL manual updates. Whoever makes the change will just open the spreadsheet, fill in the necessary records, and save the file with a new version number on the department network share"
"We don't have any business data ingestion, we only load source data, and it is never modified"
"We export the data from the web report into Excel. We copy/paste the data into another Excel sheet where we execute some VBscript macros that were developed by a guy who left the company. After that, the updated data are saved as a csv file on a network drive. The file is then picked up by a batch job and loaded into the database. The job is scheduled to run every 15 minutes. The updated records can then be displayed in our reports." - Xavier Bizoux (https://www.linkedin.com/in/xavierbizoux)
Source comment: https://www.linkedin.com/feed/update/urn:li:activity:7183406509450547200?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7183406509450547200%2C7184815374624772096%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287184815374624772096%2Curn%3Ali%3Aactivity%3A7183406509450547200%29
Coupled with:
"We don't load our data to the DataWarehouse, it's too difficult. We just stick it all in the team ACCESS database on the PC under the operations desk"
What are YOUR horror stories?
#masterdatamanagement #dataops #datagovernance
-->
<!-- Image prompt:
A spooky-but-professional flat-design illustration in the Data Controller brand style for a "Horror Stories of Business Data Ingestion" post: a haunted-house style scene rendered in dark navy, with ghostly spreadsheets, a broken ETL pipeline (cracked pipe segments leaking data rows), an old Microsoft Access database icon lurking under a desk, and a graveyard of tombstones labelled with file versions like "mapping_v12_FINAL_v2.xlsx". A full moon and a few bats in the background. Teal/green and orange accents for the eerie glow on the data elements. Humorous, Halloween-flavoured but still suitable for a B2B data product site. Minimal text. 16:9 landscape, suitable as a blog/feed cover image.
-->
@@ -0,0 +1,72 @@
---
title: 'Struggling with Legacy Data Ingestion?'
description: Filename conventions, network drives, ancient formats and 5am batch failures - legacy spreadsheet ingestion hurts everyone. With Data Controller for SAS®, those issues disappear.
date: '2026-08-24 09:00:00'
author: 'Allan Bowe'
authorLink: https://www.linkedin.com/in/allanbowe/
tags:
- Announcements
previewImg: './legacy-data-ingestion.jpeg'
---
Do you struggle against legacy data ingestion processes?
Are your analysts saving spreadsheets with a particular filename, on a particular network drive, on a particular day, with a specific structure?
Perhaps your process necessitates an ancient format (.xls, Excel '95) or particular encoding.
Sucks, right?
It sucks also for the Platform Manager, who gets the email at 5am when the batch breaks due to something not-quite-right in said input file.
Or the Data Engineer, spending all day knocking out generic excel import routines. Or the sponsor, paying for a broken process, that should take seconds.
With Data Controller for SAS®, all these issues... disappear.
Thanks to our OEM licence of [SheetJS](https://sheetjs.com), analysts can self-serve data uploads via the browser - no (insecure) network drive needed, nor thick client install.
And the data really can be **anywhere** in the workbook - in any shape, spread across any number of disparate sheets, ranges and cells. A range of [import options](https://docs.datacontroller.io/excel/) handles everything from clean tabular sheets to sprawling financial reports.
Once automatic validations and DQ checks pass, and approval received, the target table is updated and any onward jobs are triggered. Needless to say, there is a full audit trail right back to the original excel file.
Not only that - you can load ANY database, SAS dataset, format, or CAS table. New data targets are configured by the admin - zero code, nothing to deploy or drag through to production.
There is also a [community edition](https://datacontroller.io/pricing/), free for unlimited users (restricted by rows).
If you'd like to excel by spending less time on Excel - [let's chat](https://datacontroller.io/contact/).
<!--
Source LinkedIn post:
Do you struggle against legacy data ingestion processes?
Are your analysts saving spreadsheets with a particular filename, on a particular network drive, on a particular day, with a specific structure?
Perhaps your process necessitates an ancient format (.xls, Excel '95) or particular encoding.
Sucks, right?
It sucks also for the Platform Manager, who gets the email at 5am when the batch breaks due to something not-quite-right in said input file.
Or the Data Engineer, spending all day knocking out generic excel import routines. Or the sponsor, paying for a broken process, that should take seconds.
With Data Controller for SAS®, all these issues... disappear.
Thanks to our OEM licence of SheetJS, analysts can self-serve data uploads via the browser - no (insecure) network drive needed, nor thick client install.
The data can be anywhere in the workbook - in any shape, across any number of disparate sheets, ranges and cells - thanks to a range of import options. Columns in any order. Additional columns (not in the target table) are simply ignored. Invalid columns cause immediate rejection, so the uploader can rectify.
Once automatic validations and DQ checks pass, and approval received, the target table is updated and any onward jobs are triggered. Needless to say, there is a full audit trail right back to the original excel file.
Not only that - you can load ANY database, #SAS dataset or CAS table. New data targets are configured by the admin - zero code, nothing to deploy or drag through to production.
There is also a community edition, free for unlimited users (restricted by rows).
If you'd like to excel by spending less time on Excel - let's chat.
#datamanagement #excel #datagovernance
-->
<!-- Image prompt:
A flat-design illustration in the Data Controller brand style (dark navy background, teal/green and orange accents), composed around a SINGLE CENTRAL focal point so it survives a square crop: in the centre, a large glowing browser window with a spreadsheet file being dropped into it and a green tick, feeding a short downward flow into a database cylinder directly beneath. Sinking into shadow at the very bottom of the frame, small and half-buried, the legacy relics being left behind: a floppy disk labelled .xls, a dusty network drive wrapped in chains, and an alarm clock showing 5am. Light radiates from the central browser window against the dark background. All key elements within the central square of the frame; outer left and right thirds contain only background texture and glow, safe to crop. Minimal text, no logos. Professional but lightly humorous, suitable for a B2B data product site. 16:9 landscape, 1200x627, suitable as a blog/feed cover image.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+72
View File
@@ -0,0 +1,72 @@
---
title: 'SAS-isms: Library Info'
description: Expanding a VIEW library in Data Controller surfaces engine, size, paths, table count and schema - with the same data queryable from the underlying tables.
date: '2026-08-11 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- SAS-isms
previewImg: './library-info.jpeg'
---
# Library Info
One of many "SAS-isms" in Data Controller for SAS®.
When expanding a VIEW library, Data Catalog items are surfaced:
- Library Engine
- Library Size (Bytes, MB, GB)
- Paths (if BASE engine)
- Table Count
- Schema (if a database)
![Library engine and host information](./library-enginehost-info.jpeg)
And more. Much of this data can also be queried from the underlying tables:
- [MPE_DATACATALOG_LIBS](https://docs.datacontroller.io/tables/mpe_datacatalog_libs) - engine, paths, permissions, owners, schemas, metadata name / id
- [MPE_DATASTATUS_LIBS](https://docs.datacontroller.io/tables/mpe_datastatus_libs) - frequently changing attributes: library size, table count, catalog count
The split is deliberate - stable attributes live in the DATACATALOG tables, volatile ones (size, counts) in DATASTATUS. All of it is stored with SCD2, so you can track how your data landscape changes over time.
Refreshes are driven by the `mpe_refreshlibs` and `mpe_refreshtables` macros - any user can refresh an individual library from the VIEW menu, and admins can scan the entire estate from the System page.
Similar information is available for datasets - it's essentially an extract from `proc contents`, using the [mp_dsmeta](https://core.sasjs.io/mp__dsmeta_8sas.html) macro from the [SASjs Core](https://github.com/sasjs/core) library.
![Dataset attributes](./dataset-attributes.jpeg)
<!--
LinkedIn version (plain text - no markdown, links in comments):
One of many "SAS-isms" in Data Controller for SAS® - "Library Info"
When expanding a VIEW library, Data Catalog items are surfaced:
▪️ Library Engine
▪️ Library Size (Bytes, MB, GB)
▪️ Paths (if BASE engine)
▪️ Table Count
▪️ Schema (if a database)
And more. Much of this data can also be queried from the underlying tables (links in the comments below):
▪️ MPE_DATACATALOG_LIBS - engine, paths, permissions, owners, schemas, metadata name / id
▪️ MPE_DATASTATUS_LIBS - frequently changing attributes: library size, table count, catalog count
The split is deliberate - stable attributes live in the DATACATALOG tables, volatile ones (size, counts) in DATASTATUS. All of it is stored with SCD2, so you can track how your data landscape changes over time.
Refreshes are driven by the mpe_refreshlibs and mpe_refreshtables macros - any user can refresh an individual library from the VIEW menu, and admins can scan the entire estate from the System page.
Similar information is available for datasets - it's essentially an extract from proc contents, using the mp_dsmeta macro from the SASjs Core library (link in the comments).
#sas #datacatalog #datagovernance #sasjs #metadata
Suggested first comment:
MPE_DATACATALOG_LIBS: https://docs.datacontroller.io/tables/mpe_datacatalog_libs
MPE_DATASTATUS_LIBS: https://docs.datacontroller.io/tables/mpe_datastatus_libs
mp_dsmeta macro: https://core.sasjs.io/mp__dsmeta_8sas.html
Attach library-info.jpeg (and optionally library-enginehost-info.jpeg + dataset-attributes.jpeg) to the post.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

@@ -0,0 +1,85 @@
---
title: 'Oops! Now You Can Roll Back Data Changes'
description: Despite all the checks in Data Controller for SAS®, sometimes the wrong updates get approved. You can now roll back to a previous version - with full audit history preserved.
date: '2026-08-20 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- Announcements
previewImg: './ctrl-z.jpeg'
---
# Oops! Now You Can Roll Back Data Changes
Despite the MANY checks and guarantees in Data Controller for SAS®, sometimes it can happen that the wrong updates are approved and applied.
Thankfully, it is now possible to **roll back** data changes to a previous state.
![Two trains colliding - the wrong data changes applied](rollback-trains-meme.jpeg)
## How it works in practice
Behind the scenes, rollback is not a silent undo. It is a **first-class approval workflow** just like any other edit. When you choose to restore a previous version, the backend - via the [`%mp_stripdiffs`](https://core.sasjs.io/mp__stripdiffs_8sas.html) macro - reads the `MPE_AUDIT` table (or a custom `AUDIT_LIBDS` configured for the table) and computes every difference between the current state and the version you want to go back to.
It handles all three change types:
- **Deleted rows** are re-inserted with their original values.
- **Modified rows** are reverted to their previous values.
- **Added rows** are marked for deletion with `_____DELETE__THIS__RECORD_____="Yes"`.
The computed differences are written to a new staging package in the approvals directory, complete with a CSV and a `macvars.sas` snapshot of the session context. A new `LOAD_REF` is generated, and the package is submitted via the standard `%mpe_loader` service - so it goes through the same edit-stage-approve workflow as any manual change.
This means the rollback itself is **reviewable and approvable**. Nothing is applied silently, and the full audit trail is maintained: the reversion appears as a new load reference in `MPE_SUBMIT`, `MPE_REVIEW`, `MPE_DATALOADS`, and `MPE_AUDIT`, just like any other submission.
## Security and access
Not everyone can roll back everything. The `%mpe_checkrestore` macro enforces a strict access check before the restore service will run:
- The load must actually exist and have been loaded (no rollbacks of unapproved submissions).
- The table must be configured with an audit table.
- The user must have `EDIT` access to the target table.
- If the user is not an admin, Row Level Security or Column Level Security rules on the table will block the restore.
If access is denied, the service aborts with a clear reason - no opaque errors.
## What this means for compliance
Because the rollback creates a new, approved changeset rather than silently rewinding history, auditors can see exactly what was reverted, when, and by whom. The `MPE_AUDIT` table retains the record of every intermediate state, so nothing is ever truly lost. For tables where data integrity is critical - regulatory reporting, actuarial assumptions, steering parameters - this is the difference between "we have no idea what happened" and "here is the complete chain of custody."
This feature works for all temporal-aware load types (`UPDATE`, `TXTEMPORAL`, and `BITEMPORAL`) and respects SCD2 validity windows. And the whole process is built on the same open-source macro library that powers the rest of Data Controller - so you can inspect, test, and audit the code itself.
Full documentation is here: https://docs.datacontroller.io/restore/
<!--
Source LinkedIn post:
"Can we put it back the way it was?" should never be a hard question to answer.
Yet for many teams managing reference data, mappings, and regulatory adjustments in SAS®, a wrong approval means exactly that — a forensic exercise, an uncomfortable audit conversation, and a quiet hope that nobody upstream consumed the bad data.
Data Controller now lets you ROLL BACK a table to any previous state.
Not by silently rewinding history - the one thing your auditor definitely does not want. Instead, the reversion is packaged as a brand NEW change that goes through the same review and approval as any other edit:
- The reversion diff is calculated and staged
- An approver reviews it before anything is applied
- The rollback itself lands in the audit trail - who, what, when, why
Nothing disappears. The original mistake, its correction, and every state in between all remain fully traceable.
Access rules apply exactly as they do for edits - the same group permissions, the same restrictions. A rollback cannot be used as a shortcut around your controls.
For regulated reporting data - the kind where "oops" is a reportable event, not a shrug - this closes the loop between catching an error and evidencing its correction.
The underlying process is open source.
Link in the comments below 👇
#sas #sasviya #datagovernance #datamanagement
-->
<!-- Image prompt:
A dramatic flat-design meme illustration in the Data Controller brand style: two high-speed trains colliding head-on at the center of the frame, with data rows and spreadsheet cells flying out of the impact. A group of onlookers in business-casual attire stand in the foreground, heads bowed, looking on in shared sadness. Dark navy background, teal/green and muted orange accents. The collision represents conflicting data changes; the sad onlookers are the data stewards. Minimal text, no labels. Slightly stylised, not gory - suitable for a B2B data product audience. 16:9 landscape, suitable as a blog/feed cover image.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

@@ -0,0 +1,90 @@
---
title: 'Data Controller for SAS® - Use Cases and Features'
description: A rundown of Data Controller use cases and features - from secure data capture and approval workflows to SCD2 load routines, validation rules, and data lineage.
date: '2026-08-12 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- Announcements
previewImg: './usecases.jpeg'
---
# Data Controller for SAS® - Use Cases and Features
What can you actually *do* with Data Controller? Here's the rundown:
- Data viewer with search, filter, and URL links - without table locks
- Secure data (stop end-user write access to production tables)
- Configure inputs in seconds (vs days or weeks for ETL jobs)
- Preconfigured load routines including SCD2 and Bitemporal
- Avoid exposing sensitive data in unsecured staging areas
- Enable SAS workflows (trigger a SAS job on data change)
- Prevent spreadsheet changes from halting batch flows
- Email alerts when data is changed / about to change
- Audit manual overrides on regulatory reporting data
- Four-eyes (or more) approval for all data changes
- View and amend formats in SAS format catalogs
- Export DDL in SAS, TSQL or PGSQL format
- Explore SAS 9 metadata and data lineage
- Work with special SAS missing numerics
- Upload password-protected Excel files
- Upload directly from CSV or Excel
- Composite / retained key support
- Enforce data quality at source
- Column and Row Level Security
- Advanced validation rules
Our software is used and trusted (in production) by:
- Governments
- Insurers
- Pharma
- Banks
Plus various other industry sectors - references available.
Would you like to kick the tyres? [Get in touch](/contact) for details.
<!--
Source LinkedIn post:
Data Controller for SAS® Use Cases and Features:
✓ Data viewer with search, filter, and URL links - without table locks
✓ Secure data (stop end-user write access to production tables)
✓ Configure inputs in seconds (vs days or weeks for ETL jobs)
✓ Preconfigured load routines including SCD2 and Bitemporal
✓ Avoid exposing sensitive data in unsecured staging areas
✓ Enable SAS workflows (trigger a SAS job on data change)
✓ Prevent spreadsheet changes from halting batch flows
✓ Email alerts when data is changed / about to change
✓ Audit manual overrides on regulatory reporting data
✓ Four-eyes (or more) approval for all data changes
✓ View and amend formats in SAS format catalogs
✓ Export DDL in SAS, TSQL or PGSQL format
✓ Explore SAS 9 metadata and data lineage
✓ Work with special SAS missing numerics
✓ Upload password-protected Excel files
✓ Upload directly from CSV or Excel
✓ Composite / retained key support
✓ Enforce data quality at source
✓ Column and Row Level Security
✓ Advanced validation rules
Our software is used and trusted (in production) by:
- Governments
- Insurers
- Pharma
- Banks
Plus various other industry sectors - references available.
Would you like to kick the tires? DM for details.
#sas #sasviya #dataquality #datacapture
Image prompt:
Flat vector illustration, dark slate blue background (#314351), wide 1.91:1 banner. A clean modern data dashboard interface floating in the centre showing a data grid/table with a search bar and filter icons. Around it, a neat ring of small white and light-grey icons representing governance features: a padlock (security), a magnifying glass over rows (audit), two checkmarks side by side (4-eyes approval), an envelope (email alerts), an Excel/spreadsheet icon with an upward arrow (upload), a branching flow diagram (workflow/lineage), and a shield with a tick (data quality). Thin connecting lines link the icons to the central dashboard, suggesting control and flow. Minimal, professional, corporate SaaS style; subtle depth with soft shadows; accent colour a muted red-orange (#d3531a) used sparingly on one or two icons. No text, no words, no letters. High contrast, crisp edges, suitable as a LinkedIn post image.
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

+1 -1
View File
@@ -3,7 +3,7 @@ title: 'v7.12.0: Regex Validation Rules'
description: Define regex patterns once and have every value checked automatically - with hard rules that block bad data and soft rules to flag it for a second look.
date: '2026-07-28 15:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data_controller
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- Announcements
---
+1 -1
View File
@@ -3,7 +3,7 @@ title: "Welcome to the Data Controller Feed"
description: A new home for quick announcements, social media updates and other bite-sized news from the Data Controller team.
date: '2026-07-17 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data_controller
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
tags:
- Announcements
---
@@ -3,7 +3,7 @@ title: 'Zen and the Art of Data Quality'
description: Pirsig's motorcycle classic meets the National Bank of Belgium's DQ guidance - and the three layers of data quality defence Data Controller for SAS provides.
date: '2026-08-07 09:00:00'
author: 'Data Controller'
authorLink: https://www.linkedin.com/showcase/data_controller
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
previewImg: './dilbert.jpeg'
tags:
- Announcements
@@ -0,0 +1,26 @@
---
title: 'Zero-Code Business Data Capture for SAS®'
description: Loading business data to IT-controlled databases should be simple, secure, and FAST. Data Controller for SAS® eliminates hand-cranked ETL flows.
date: '2026-08-10 18:00:00'
author: 'Allan Bowe'
authorLink: https://www.linkedin.com/in/allanbowe/
tags:
- Announcements
previewImg: './zero-code-business-data-capture-for-sas.jpeg'
---
As a member of a Data & Analytics team in a regulated sector such as Banking or Insurance, you need to deliver outputs in a consistent and timely fashion to meet statutory and management reporting demands.
In order to do that, your team builds and maintains automated processes that capture, cleanse and integrate with centrally managed Data Warehouses.
The problem when capturing data created by business users (as opposed to IT-secured sources) is the cost to build, deploy and incident-manage ETL flows with constantly changing file formats and arrival frequencies.
This can result in 'blame-games' and delays in data delivery.
We strongly believe that loading business data to IT-controlled databases should be simple, secure, and fast.
That's why we created Data Controller for SAS® - a unique zero-code solution for capturing business data at source and moving it safely and securely into your IT-managed data store.
The result is the complete elimination of 'hand-cranked' ETL flows and other expensive home-grown solutions that eat up your OpEx budget and reduce your capacity to scale.
To see how easily Data Controller can be deployed in your organisation, book a [Discovery Call](https://datacontroller.io/contact/).
Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

-36
View File
@@ -1,36 +0,0 @@
# Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Before exploring, read these
- **`CONTEXT.md`** at the repo root, or
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
## File structure
Single-context repo (most repos):
```
/
├── CONTEXT.md
├── docs/adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
## Use the glossary's vocabulary
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
-30
View File
@@ -1,30 +0,0 @@
# Issue tracker: Local Markdown
Issues and specs (you may know a spec as a PRD) for this repo live as markdown files in `.scratch/`.
## Conventions
- One feature per directory: `.scratch/<feature-slug>/`
- The spec is `.scratch/<feature-slug>/spec.md`
- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` — never a single combined tickets file
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
## When a skill says "publish to the issue tracker"
Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
## When a skill says "fetch the relevant ticket"
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a file with one **child** file per ticket.
- **Map**: `.scratch/<effort>/map.md` — the Notes / Decisions-so-far / Fog body.
- **Child ticket**: `.scratch/<effort>/issues/NN-<slug>.md`, numbered from `01`, with the question in the body. A `Type:` line records the ticket type (`research`/`prototype`/`grilling`/`task`); a `Status:` line records `claimed`/`resolved`.
- **Blocking**: a `Blocked by: NN, NN` line near the top. A ticket is unblocked when every file it lists is `resolved`.
- **Frontier**: scan `.scratch/<effort>/issues/` for files that are open, unblocked, and unclaimed; first by number wins.
- **Claim**: set `Status: claimed` and save before any work.
- **Resolve**: append the answer under an `## Answer` heading, set `Status: resolved`, then append a context pointer (gist + link) to the map's Decisions-so-far in `map.md`.
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = {
summary: ``
},
social: {
linkedin: `https://www.linkedin.com/showcase/data_controller/`
linkedin: `https://www.linkedin.com/showcase/data-controller-for-sas/`
}
},
pathPrefix: '',
+5
View File
@@ -21,6 +21,11 @@ const Footer = () => (
on <StyledAnchor href="https://sasapps.io">SAS Web Apps</StyledAnchor>
.
</StyledDesc>
<StyledDesc>
<StyledAnchor href="https://sasapps.io/cyber-essentials-certified/">
Cyber Essentials Certified
</StyledAnchor>
</StyledDesc>
</div>
<div className="col-md-3">
<StyledHeading>Source Code</StyledHeading>
+11 -1
View File
@@ -1,7 +1,8 @@
import React from 'react'
import { Link } from 'gatsby'
import { FaShieldAlt } from 'react-icons/fa'
import { Hero, HeroHeading, HeroDesc } from './style'
import { Hero, HeroHeading, HeroDesc, HeroBadge } from './style'
import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents'
import { Container } from '../shared'
@@ -19,9 +20,18 @@ const HeroSection: React.FC<HeroSectionProps> = ({
<HeroHeading>{heading}</HeroHeading>
<HeroDesc>{desc}</HeroDesc>
{location.pathname === pathPrefix + '/' && (
<>
<HeroBadge
href="https://sasapps.io/cyber-essentials-certified/"
target="_blank"
rel="noopener"
>
<FaShieldAlt /> Cyber Essentials Certified
</HeroBadge>
<Link to="/contact/">
<OutlineButton>Try Data Controller</OutlineButton>
</Link>
</>
)}
</Container>
<BottomSectionArrow />
+17
View File
@@ -17,3 +17,20 @@ export const HeroHeading = styled.h1`
export const HeroDesc = styled.p`
opacity: 0.8;
`
export const HeroBadge = styled.a`
display: block;
width: fit-content;
margin: 0.75rem 0 0;
padding: 0.375rem 1rem;
font-size: 0.75rem;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 0.25rem;
color: white;
text-decoration: none;
&:hover {
color: white;
border-color: white;
opacity: 0.9;
}
`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB