In the previous article, I built a personal LLM Wiki that does not leave project knowledge trapped inside ChatGPT Memory. The useful state that survives a conversation is persisted in GitHub and Obsidian instead.
Once project state can live in GitHub, the next question follows naturally:
If ChatGPT commits a change to GitHub, can the next operation run automatically?
Suppose ChatGPT updates a configuration file. If a person still has to SSH into the server, run `git pull`, execute a test script, and inspect the result, knowledge management has been automated but execution has not.
The simplest idea is to give the AI direct access to the server shell.
ChatGPT
↓
SSH / Shell
↓
Server
Technically, that works. Operationally, it creates harder questions. What command did the AI actually run? Which code version did it execute? What happens if the same request arrives twice? Is a failed command safe to retry? How do we prevent the AI from generating a command that was never meant to be allowed?
So I use the opposite design:
Do not give the AI execution authority. Let the AI record a change in Git, and let the runtime interpret that change and execute only predefined operations.
Use the Git Commit as an Execution Contract

The architecture is:
ChatGPT
↓
GitHub Commit
↓
GitHub Push Event
↓
n8n
↓
Policy / Routing
↓
FastAPI
↓
OCI Runtime
↓
Test / Validation
↓
PASS / FAIL
Each component has a narrow responsibility. ChatGPT decides what should change and creates the Git change. GitHub is the control plane that preserves history and the commit SHA. n8n receives the event, applies policy, and routes the request. FastAPI maps an approved action to a runtime operation. OCI performs deterministic work such as testing, validation, or building.
The key distinction is that ChatGPT does not generate the shell command that the server will execute. ChatGPT creates the Change. The runtime decides how that change is executed.
The Demo We Will Build
The PoC uses one deliberately small scenario. ChatGPT or a developer commits a change to a specific GitHub repository. A GitHub Push event triggers an n8n workflow, and n8n checks whether the repository is allowed, whether the branch is allowed, whether a real commit SHA exists, and whether every changed file is inside an approved path.
If the event passes those checks, n8n asks the FastAPI service running on OCI to perform an action. FastAPI never takes an arbitrary string and passes it directly to a shell. It accepts only registered actions.
validate
→ pytest
build
→ predefined build script
lint
→ predefined lint command
This example uses only `validate`.

Step 1. Receive GitHub Push as an n8n Event
Use the GitHub Trigger node in n8n. Connect a GitHub credential and configure the repository and Push event.
Repository Owner: YOUR_GITHUB_ID
Repository Name: demo-agent
Events: Push
After the workflow is activated, pushing a commit to the repository starts the workflow. The Push payload contains several fields that matter for the execution contract:
repository.full_name
ref
after
commits[]
`after` is the commit SHA referenced by the ref after the push completes. This is important because reproducible execution should mean execute this commit, not execute whatever happens to be on main now.
Step 2. Make n8n a Policy Layer, Not a Relay
Receiving a webhook and immediately calling the server is not enough. n8n should apply a minimum policy first.
GitHub Trigger
↓
Normalize / Policy
↓
Allowed?
↙ ↘
NO YES
Stop FastAPI
A Code node after the GitHub Trigger can normalize the payload and validate repository, ref, commit SHA, and changed paths. A representative policy looks like this:
const payload = $json.body ?? $json;
const ALLOWED_REPOSITORY = 'YOUR_GITHUB_ID/demo-agent';
const ALLOWED_REF = 'refs/heads/main';
const repository = payload.repository?.full_name;
const ref = payload.ref;
const commitSha = payload.after;
const commits = payload.commits ?? [];
const changedFiles = [
...new Set(
commits.flatMap((commit) => [
...(commit.added ?? []),
...(commit.modified ?? []),
...(commit.removed ?? []),
])
),
];
const shaIsValid =
typeof commitSha === 'string' &&
/^[0-9a-f]{40}$/i.test(commitSha) &&
!/^0{40}$/.test(commitSha);
const allowedPaths = [
'config/',
'automation/',
'tests/',
];
const filesAreAllowed =
changedFiles.length > 0 &&
changedFiles.every((file) =>
allowedPaths.some((prefix) => file.startsWith(prefix))
);
const allowed =
repository === ALLOWED_REPOSITORY &&
ref === ALLOWED_REF &&
shaIsValid &&
filesAreAllowed;
return [{
json: {
allowed,
repository,
ref,
commitSha,
changedFiles,
action: 'validate',
},
}];
Repository and branch allowlisting are policy problems, not reasoning problems. They should therefore be handled by deterministic code rather than delegated to an LLM.
Separate What an Agent Should Decide from What Code Should Decide
A common mistake in Agentic Workflow design is to hand every decision to the LLM. But the fact that an LLM can make a decision does not mean it should.
The agent is useful for ambiguous work: understanding intent, deciding what should change, interpreting context, or producing a patch. Deterministic code is better for repository allowlists, branch checks, SHA validation, path policies, retries, state transitions, and execution permissions.
In this architecture, AI owns Intent and Change. The system owns Policy and Execution.
Step 3. Forward Only Approved Events to FastAPI
Add an IF node after the policy Code node. Only the `allowed === true` branch calls FastAPI. The request should carry structured data such as the repository, commit SHA, and approved action rather than an arbitrary command string.
{
"repository": "YOUR_GITHUB_ID/demo-agent",
"commit_sha": "3d7f5e8...",
"action": "validate"
}
The server then decides what `validate` actually means.
Step 4. Build an Allowlisted Executor in FastAPI
FastAPI should restrict actions to an enum or equivalent allowlist. The execution layer must remain a final policy boundary even if the n8n workflow is accidentally changed or another client calls the API directly.
The essential rule is simple: never construct a shell command from untrusted external input. Map a small set of approved action names to predefined argument arrays and execute with `shell=False`.
For this PoC, `validate` maps to a fixed pytest invocation:
python -m pytest -q
The repository path is also resolved from server-side configuration rather than accepted as an arbitrary filesystem path from the caller.
Step 5. Execute the Exact Commit, Not the Branch

FastAPI now works against the commit SHA received from GitHub. Instead of testing whichever revision is currently checked out on the server, fetch the repository and create a detached Git worktree for the exact event commit.
git -C /srv/repos/demo-agent fetch origin
git -C /srv/repos/demo-agent worktree add
--detach
/tmp/jobs/job-001
3d7f5e8...
This isolates the execution target. A later push to `main` cannot silently change what the current job validates.
After the operation finishes, remove the worktree.
git -C /srv/repos/demo-agent worktree remove
--force
/tmp/jobs/job-001
A practical executor therefore follows the same sequence: resolve the repository from an allowlist, validate the commit SHA, fetch, create a detached worktree, run the predefined command in that worktree, capture stdout/stderr, enforce a timeout, and clean up the worktree in a `finally` path.
For the validation action, the process invocation can remain explicit:
subprocess.run(
[
"python",
"-m",
"pytest",
"-q",
],
cwd=worktree_path,
capture_output=True,
text=True,
timeout=300,
shell=False,
)
The important property is not the framework. It is that external data never becomes a shell command.
Step 6. Job State Matters More Than HTTP 200
At this point the demo works, but a production workflow needs more than a successful HTTP response. Execution should have durable state.
A minimal job record can preserve the repository, commit SHA, action, status, timestamps, and execution evidence. That makes it possible to answer operational questions later: Has this commit already been processed? Is the job running, passed, or failed? Can it be retried safely? Which exact code revision produced the result?

This small addition makes the automation much more reliable because the workflow becomes traceable and duplicate execution can be controlled explicitly.
Verify Signatures When Receiving Webhooks Directly
This example uses n8n’s GitHub Trigger. If you instead expose your own webhook endpoint, validate the GitHub webhook signature before trusting the payload.
Security should not be a single check immediately before execution. Narrow trust repeatedly across boundaries: GitHub authenticates the event, n8n enforces event policy, FastAPI enforces the action allowlist, and the runtime executes a fixed command against an isolated commit.
Why Not Just Use GitHub Actions?
GitHub Actions is a strong choice for CI/CD. If the requirement is simply to run tests or builds after a push, Actions may be the simpler tool.
The reason to introduce n8n is different: the Git event may be only one step in a wider business workflow. The same flow may need approval, notifications, CRM updates, document processing, AI calls, or routing across systems. In that case, n8n can act as the orchestration and policy layer while OCI remains the controlled runtime.
The boundary can also be split cleanly: GitHub Actions for CI, n8n for business workflow orchestration.
The Important Metric Is Not the Number of Agents
There is a temptation to turn every part of an AI system into an agent. That usually makes operations harder rather than smarter.
Use probabilistic reasoning where ambiguity exists. Use deterministic control where correctness, security, replayability, and state transitions matter. This is the separation I consider central to Enterprise AI Architecture: Deterministic Control vs. Probabilistic Reasoning.
Connecting This to Part 1
The previous architecture persisted useful project context from ChatGPT into GitHub and Obsidian. This article adds an execution path to that control plane.
Conversation
↓
Reasoning / Change
↓
GitHub
↓
Event
↓
Policy
↓
Deterministic Runtime
↓
Evidence / State
GitHub is no longer only a place to store code or notes. It becomes the boundary between AI-generated change and controlled execution.
The goal is not to give AI more authority. The first priority is to establish a clear contract between the AI’s nondeterministic reasoning and the system’s deterministic execution.
And This Is Where a Real Agent Can Enter
This article intentionally keeps AI out of the runtime. The execution path is deterministic by design.
That boundary gives us a safer place to introduce a more capable agent next. Instead of asking an agent to invent shell commands, we can give it a bounded mission: inspect context, plan a multi-step task, produce an auditable change, and hand the result back through the same Git-based contract.
The agent can become more capable without turning the production runtime into an unrestricted shell. That is the direction worth extending.