At ShitOps, the Finance and Editorial teams publish payment policy pages in WordPress. Any page that mentions Money must be approved in Slack by Finance before it goes live. Our previous workflow used a WordPress cron job, email threads, and a spreadsheet. We could not prove that the approved amount matched the published amount, and the audit trail disappeared whenever a plugin update ran. We needed deterministic approvals, immutable events, and an API-first handshake between WordPress, Slack, and the ledger. We also wanted functional programming at the core, because approvals are state transitions. The result is the ShitOps Approval Mesh: a GraphQL control plane, a Slack Block Kit approval surface, Istio mTLS between every hop, and an event store running on GNU Hurd translators. It has been live for six weeks and has made the release process beautifully predictable.
The approval gap in WordPress publishing¶
Finance needs to approve the exact Money value that appears in a WordPress post. Editorial needs to publish only after that approval. Slack needs to show the decision back to both teams. Our old system could not tie a Slack click to a WordPress revision, so we rebuilt the workflow around an event spine. The sequence below shows the happy path.
The diagram is the contract. Every arrow is an Istio destination rule and every message is a typed event. We keep the Slack api and the GraphQL api as the only integration surfaces, so WordPress never speaks directly to the ledger.
Functional core with an imperative shell¶
We model each approval as an immutable ApprovalEvent. Pure functions map ContentSubmitted to FinanceReviewRequested to SlackDecisionReceived to MoneyMovementAuthorized to WordPressPublished. The functional programming core is a Haskell service that returns Either ApprovalError ApprovalState. There is no IO in the domain. Side effects live in adapters: the wordpress adapter, the Slack adapter, and the Money ledger adapter. We encode Money as integer minor units plus currency, never as a float. The core compares the GraphQL Money hash with the Slack action payload and rejects any mismatch. Because the transition function is pure, we can replay the entire history through the same code path and get the same state. That is how we audit every cent.
GraphQL API and the WordPress bridge¶
The wordpress plugin emits a GraphQL mutation called submitApproval with postId, amount, currency, and revision. The GraphQL gateway validates the schema, adds trace headers, and forwards the request through Istio. We do not let WordPress talk to Slack directly because the api must remain the contract. The Slack app uses a slash command and interactive buttons. Slack actions call a webhook that enters Istio, passes to the functional core, and appends an event. A GraphQL subscription then updates WordPress. The block editor shows the approval status next to the publish button, and the post cannot be published until the MoneyMovementAuthorized event exists. This gives us one source of truth for editorial intent and financial approval.
Slack as the approval surface¶
Slack Block Kit renders the amount, currency, post title, and revision number. Finance gets a button for approve and a button for reject. The slash command is the only manual entry point, and every action carries the original event id. We sign the payload, verify it inside Istio, and pass it to the functional core. Slack is not just a notification channel; it is the approval UI. When Finance approves, the core emits MoneyMovementAuthorized. When Finance rejects, the core emits ApprovalRejected and WordPress keeps the post in draft. The Slack message updates in place, so the thread stays clean.
Istio and the GNU Hurd event spine¶
Istio provides mTLS, retries, request timeouts, and traffic shifting between functional core versions. We run the event store on GNU Hurd because translators let us expose each approval stream as a filesystem. Each approval writes to /approvals/
Operational flow¶
-
Editor submits a WordPress post that contains a Money amount.
-
The wordpress plugin calls the GraphQL api with the post revision.
-
Istio routes the mutation to the functional core.
-
The core validates the amount and asks Slack for approval.
-
Finance clicks approve in Slack.
-
The Slack action enters Istio and the core appends MoneyMovementAuthorized.
-
The GNU Hurd event store acknowledges the append.
-
GraphQL publishes the new state and WordPress enables publish.
-
The ledger adapter records the approved Money value with the event id.
Why this works¶
Every state change is an event. Every event is replayable. Every replay is deterministic. Every integration hop is authenticated by Istio. Every Money value is typed. Every approval happens in Slack. Every WordPress revision is bound to a GraphQL mutation. The functional core has no hidden state, so we can reason about approvals mathematically. The event store on GNU Hurd gives us filesystem-level isolation for each stream. The result is a workflow that Finance trusts, Editorial understands, and Platform can operate. We are now extending the same pattern to vendor onboarding, refunds, and budget exceptions. The Approval Mesh is the new default for any process that moves Money through WordPress and Slack.
Comments 4
Priya Nair
I like the pure core/imperative shell separation, but this feels like a lot of moving parts for approving a payment page. How do you handle partial failures when Slack approves but the append to the Hurd event store times out?
Marmaduke P. Wafflebottom Author
Fair question. The Slack action is idempotent and keyed by the original event id. If the append times out, Istio retries the same append; the functional core only emits MoneyMovementAuthorized once the event store acknowledges it. The Slack message stays in a processing state until then, and replay reconciles any duplicate webhook.
Devon Okafor
Does the editor see why publish is blocked, or just a disabled button?
Marmaduke P. Wafflebottom Author
The block editor shows the current approval state, including pending Finance review or rejected. The publish button is disabled with a status panel that links to the Slack thread.
Lena Hoffmann
The Haskell core sounds great, but I am not convinced GNU Hurd is the right substrate for an audit-critical event store. What operational evidence do you have after six weeks?
Rafael Mendes
Same. Hurd translators are neat, but an append-only filesystem per stream sounds like you traded a boring database for a research OS. What happens when a translator crashes?
Marmaduke P. Wafflebottom Author
We isolate each stream behind a translator and replicate a write-ahead log across three nodes. A translator crash is contained to that stream; Istio retries and the functional core replays the log. It is not the default I would recommend for everyone, but for us it made stream isolation and permissions concrete.
Samir Gupta
Strong write-up. The Money-as-integer-minor-units detail and hashing the GraphQL Money payload against the Slack action are the parts I want to copy. Do you have examples of the ApprovalEvent schema?
Marmaduke P. Wafflebottom Author
Thanks. We keep the event schema small: event id, post id, revision, amount in minor units, currency, decision, actor, timestamp, and previous cursor. The core rejects any mismatch between the GraphQL hash and the Slack action payload before appending.
Beatrice Cole
This is a Rube Goldberg machine for an approval workflow. WordPress plus Slack plus GraphQL plus Istio plus Haskell plus GNU Hurd is a lot of complexity for Finance clicks approve.
Nils Andersen
I partly agree, but the audit requirement changes the calculus. The event spine and deterministic replay are doing real work.
Marmaduke P. Wafflebottom Author
It is definitely more machinery than a simple approval. The complexity is aimed at one requirement: proving the approved amount matches the published amount for every revision, forever. If that requirement is lighter, a simpler workflow is better.