We recently set out to build a demo app that showcases what Centrali can do as a platform for internal tools. The result is the Platform Engineering Hub — a governed self-service portal where developers request GitHub repositories, approvers review and approve them, and the system automatically provisions repos with the right policies applied.
Along the way, we hit a design problem that led us to orchestrations — and it turned out to be the perfect use case. Here's the story.
What We Were Building
The Platform Engineering Hub manages the full lifecycle of GitHub repositories through a governance workflow:
- A developer submits a request ("Create a new private service repo called payments-service")
- The system evaluates the request against policy profiles (Does it need approval? What risk level?)
- An approver reviews and approves or rejects the request
- If approved, the system provisions the repo in GitHub — creates it, applies team permissions, sets up branch protection
- Everything is tracked in an audit trail
We built all of this on Centrali: 5 collections for the data model, 7 compute functions for the business logic, event triggers to wire them together, and Pages for the UI.
The Problem: Chaining Approval to Execution
The first four steps worked independently. Event triggers fired the evaluator when a request was created. On-demand triggers let approvers approve or reject. Separate on-demand triggers could execute the GitHub provisioning.
But we needed step 3 and step 4 to happen as a single flow. When an approver clicks "Approve" and all approval steps pass, the system should automatically proceed to create the GitHub repo — without the approver needing to click another button.
Attempt 1: Event triggers (blocked by loop detection)
Our first instinct was an event-driven trigger on record_updated for governance requests. The idea: when the approve function sets the request status to "approved", a record_updated event fires, which triggers the executor function to create the GitHub repo.
It sounds clean — but when we tried to create the trigger, Centrali rejected it:
Trigger creates a self-loop: function calls updateRecord('governance_requests')
which would re-fire this trigger on record_updated for 'governance_requests'.The executor function updates the same governance_requests collection (setting status to "provisioning", then "completed"), which would fire the record_updated trigger again, which would run the executor again — an infinite loop. Centrali's loop detection caught this at trigger creation time, before it could cause damage.
This is actually a good thing. The platform is protecting us from a subtle bug that would have been painful to debug in production.
Attempt 2: We considered chaining via api.invokeTrigger
We considered having the approve function directly invoke the executor trigger when all approval steps pass. But this would embed workflow routing logic inside a function — the approve function would need to know which executor to call, handle secrets, and manage errors from the execution step. The flow becomes invisible and hard to debug.
The Pivot: Orchestrations
That's when we realized this is exactly what orchestrations are designed for. Instead of embedding workflow logic inside functions, we should use the tool that was built for multi-step workflows — with explicit steps, conditional routing, step-by-step execution history, and independent retryability.
How Orchestrations Solved It
Orchestrations are Centrali's answer to multi-step workflows. Instead of trying to chain functions through event triggers, you define a workflow with explicit steps, conditions, and routing.
Here's what our orchestration looks like:
Step 1: approve (compute)
→ Run the Approve function
→ Returns { allApproved: true/false, requestId: "..." }
Step 2: check-approved (decision)
→ If steps.approve.output.allApproved == true → go to execute
→ Otherwise → go to done (end)
Step 3: execute (compute)
→ Run the Execute Repo Create function
→ Creates the GitHub repo, applies policies, writes audit events
→ Has encrypted GitHub App secrets for API authentication
Step 4: done (compute)
→ Terminal step (end of workflow)Decision Steps Are the Key
The decision step is what makes this work. It evaluates the output of the previous step and routes the workflow accordingly. In our case:
- If the approver approved the final step and
allApprovedistrue, the workflow continues to GitHub provisioning - If there are still pending approval steps (multi-step approval), the workflow ends — the next approver will trigger a new orchestration run
The condition syntax is straightforward:
{
"path": "steps.approve.output.allApproved",
"op": "eq",
"value": true
}Encrypted Secrets on Compute Steps
The execute step needs GitHub App credentials (private key, app ID, installation ID) to authenticate with the GitHub API. Orchestrations support encrypted parameters on compute steps — secrets are encrypted at rest and only decrypted inside the sandbox at execution time.
This means the orchestration definition is safe to store in the database, and secrets never appear in logs, API responses, or the console UI. At runtime, the function sees them in triggerParams just like any other trigger parameter.
The End-to-End Flow
Here's what happens when everything is wired together:
1. Developer creates a request
A new record is created in governance_requests with status submitted.
2. Event trigger fires the evaluator
The record_created event trigger automatically runs the Evaluate function. It checks the request against the policy profile, determines the risk level, and creates approval steps. The request moves to pending_approval.
3. Approver triggers the orchestration
From the Pages UI (or via API), the approver triggers the "Approve and Execute" orchestration with the approval step ID. The orchestration:
- Runs the Approve function → marks the step as approved
- Checks if all steps are done → decision routes to execute
- Runs the Execute function → creates the GitHub repo
4. GitHub repo is provisioned
The execute function generates a GitHub App JWT, exchanges it for an installation token, creates the repo, applies team permissions, creates a governed repo record, and writes audit events. The request moves to completed.
The entire flow — from "developer clicks submit" to "repo exists in GitHub with policies applied" — runs automatically once the approver clicks approve.
What We Learned
Orchestrations solve the "chain of side effects" problem
When function A's output should trigger function B, and both functions modify the same data, event triggers create loops. Orchestrations give you explicit control over the flow without loop risks.
Decision steps enable conditional workflows
Not every approval leads to execution. Multi-step approvals need the first approver's action to end without executing. Decision steps let you express this naturally — "if all approved, execute; otherwise, stop."
Encrypted params make secrets manageable
Instead of duplicating secrets across multiple triggers (one per executor function), the orchestration holds the secrets on the specific step that needs them. One place to update when credentials rotate.
Functions need to handle multiple invocation contexts
The same function might be called via an on-demand trigger (from Pages) or as an orchestration step. The key difference: on-demand triggers pass input as executionParams, while orchestrations pass input as triggerParams. We added a simple check at the top of each function:
const params = executionParams.approvalStepId
? executionParams
: triggerParams;Build backend first, pages last
We built collections, compute functions, triggers, and orchestrations before touching Pages. This meant we could test the entire workflow via the MCP without needing a UI — and when we built the pages, everything was already wired and working.
What's Next
The Platform Engineering Hub currently handles repo creation. We're extending it to support:
- Access grant workflows — request and approve team/user access to repos
- Archive workflows — governed repo archival with lifecycle policies
- Drift detection — compare desired state in Centrali with actual state in GitHub
- Webhook ingestion — real-time updates from GitHub events into the audit trail
Each of these follows the same orchestration pattern: evaluate → approve → execute. The data model and workflow architecture are already in place.
If you're building internal tools that need approval workflows, external API integrations, or multi-step automation, orchestrations might be exactly what you need.