Inside the Ultimate Night-Shift Engine Powering My Marketing

16–24 minutes
measured on my own machine
A quiet machine room at night: dark petrol control panels, a rail of amber indicator lamps, a blank index card under a desk lamp, a clock tower in the window

Every morning around seven I open a page on port 4400 of a ten-year-old ThinkPad and find cards waiting. Someone drafted a post for X at 01:30. Someone checked at 00:50 whether the bottleneck had moved. Someone noticed that an article has been sitting unpublished for eleven days. None of these someones is a person. This is the machine room: a directory in my repository called Marketing-agency, the control room that runs the marketing for seitenhebel.de, emailmanager.ai, swiftrun.ai and, since last week, this site.

I have written about what one night looks like and what it costs. This post is about how the thing is built and how it got here. Everything below is measured from the repository this morning, not remembered. The short version: 94 days, 1,097 commits, and the real development was not that the machine can do more. It is that it stopped believing its own success reports.

The MarketingOps cockpit, team view: one human box at the top, 38 agent roles below it, each marked as working, paused or not wired
The cockpit, team view. One human at the top, 38 roles below, 13 of them working tonight. The interface is German because it is an internal tool; translating it for a screenshot would make it a prop.
1,097commits
94days
26agent loops
253test files
16tables
13ways to check

Part I

How the machine room is built

The core is not a framework. It is a loop with one hard condition: a card closes only when the outside world confirms that something happened. Not when an agent reports it. Not when a step was ticked off in a checklist.

Here is the whole night as one picture. A scheduler starts a loop. The loop is a single claude -p call with a prompt, a list of tools it may use, and a time limit between eight and 45 minutes. It has to end in a JSON artifact that is checked against a schema; if the check fails there is one rescue attempt, then the run counts as failed. A generator turns the artifact into a card. The cockpit shows me the card. I decide, and my click does something in the outside world. And then the important part: a separate step called reconcile goes and asks the outside world whether that something is actually there.

SchedulerLoop runArtifactCardCockpit cron, at night00:30 to 01:4511 of 26 loops claude -pprompt + tool listtime limit 8 to 45 min JSON, checkedagainst a schemaelse one rescue attempt a generator turns thefinding into a cardidentity = slugify(title) port 4400Georg looks,decides, clicks startsdeliversbecomesshows marketingops.db, the only durable truth 16 tables: tickets, ticket_events, artifacts, runs, deals, proposals, run_costs … writes run + cost is read in reads the card closes Georg’s click Reconcile 13 proof types askthe outside world,never the bookkeeping Outside world Buffer, git push, sitepress portal, mailboxvideo render writes: done Dispatcher tick, every 60 s inside the cockpit server: 1 generators, 2 reconcile, 3 reaper, 4 aging, 5 dispatch pass 5 starts real runs, and only with TICKETS_DISPATCH=1
Figure 1. The circle closes bottom left, not top right. An agent that reports “done” closes nothing. Reconcile asks the outside world for each of the 13 proof types: is the post really in Buffer, is the article really live, does the rendered file exist. Only that signal sets the card to done.

One file holds the truth

Everything durable lives in one SQLite file with 16 tables. Not in the agents’ memory, not in a chat history, not in a folder of markdown files that looks organised until you need to query it. Tickets, ticket events, artifacts, runs, deals, proposals, and the cost of every run go into the same file, so that any question of the form “what actually happened last night” has exactly one place to be answered from.

The night in 75 minutes

Eleven loops are scheduled and run one after another. Six wait for an explicit order. Nine are switched off, and they stay in the configuration with the date and the reason, so that anyone can see they were switched off and not forgotten. Two days ago there were fourteen on the schedule. On 6 September I switched off five loops that had produced for thirty days without a single result leaving the house, and switched two of them back on once they had an exit.

A terminal on the production ThinkPad listing eleven scheduled agent loops between 00:30 and 01:45, with the dispatch flag set to one
The schedule, read off the machine that runs it. Three of the eleven check a precondition first and exit in seconds if nothing moved. The last line is the whole safety model: the workshop leaves that flag unset, so only this machine turns a card into a run.

Scheduled, 11

  • doctrine-reviewdaily 00:30, only if expired
  • strategy-impulse1st of month 00:30
  • contrariandaily 00:40, only if channels changed
  • diagnosisdaily 00:50, only if bottleneck moved
  • analyticsSun 01:00
  • seo-blogWed + Sun 01:00
  • pr-cycleTue 01:10, 14-day cadence
  • trend-briefingFri + Sat 01:10
  • linkedinTue + Thu + Sat 01:20
  • community-xdaily 01:30
  • impulsedaily 01:45

On order only, 6

  • video-strategy25 min
  • video-draft30 min
  • ads-google30 min
  • ads-meta30 min
  • ads-linkedin30 min
  • ads-control25 min

Switched off, 9

  • outreach17 Jul
  • sales-followupoff
  • sales-inbox6 Sep
  • master-contentoff
  • cro-hypothesesoff
  • linkedin-dmoff
  • community-reddit6 Sep
  • community-fboff
  • output-grader6 Sep

What a card can go through

A card has ten states, and the transitions between them are a closed list in code, not a convention. A transition that is not in the list throws an error instead of quietly happening. That sounds pedantic until the first time an agent tries to move a card from “dismissed” back to “queued” because its prompt told it to be helpful.

inboxqueuedin_progressawaiting_reviewdone acceptedstarteddraft readyGeorg blockeddead_letterdeferred blockage gone retry “the shelf is not a one-way street” mergeddismissed double border = terminal, no way back merged: the work lives on in its target TRANSITIONS is a closed list. A transition that is not in it throws instead of quietly happening.
Figure 2. Ten states, and the transitions are a closed legal map rather than a convention. The most expensive mistake of this model is not in the picture: a card’s identity is slugify(title). Rephrase a title and you create a new card and leave the old one immortal.

Two machines, one gate

The same code base runs on two machines, and only one of them is allowed to start runs, spend money and act on the outside world. The workshop is a Mac Studio with git, where the dispatcher generates and ages cards but starts nothing. Production is a ThinkPad X260 under my desk, without git, a pure deploy copy, where the dispatch flag is set and the database is the truth. Deploying is an rsync, not a git pull: data arrives immediately, behaviour changes about four minutes later, after the restart.

Across both machines runs one gate. Agents deliver drafts. They send nothing, publish nothing, submit no form, open no account, spend no money and change no ticket state. Anything beyond that needs a paragraph in the rules file, committed before the run, that names the command, the files and the side effects. A “yes” in a chat window does nothing, and I learned that the expensive way: a rule only works where the executor is looking.

The cockpit channel view, listing each outbound channel with its state
Where a card is allowed to leave. Every channel the machine may act on, and its state. A channel with no exit produces drafts nobody sees, which is why five of them were switched off on 6 September.
Workshop Production Mac Studio~/claude-code/Marketing-agencygit: yes, part of the monorepoTICKETS_DISPATCH: not setdatabase: working copygenerates and ages cards, starts nothing ThinkPad X260~/apps/marketing-agencygit: no, a pure deploy copyTICKETS_DISPATCH=1database: the truthstarts real runs, costs money deploy.sh marketing rsync, no git pulldata at once,behaviour after the~4 min restart The gate, and the opening as its only key Agents deliver drafts. Anything more needs a paragraph in the rules file, committed before the run, naming command, files and side effects. A “yes” in chat does nothing.
Figure 3. The separation is why a mistake in the workshop stays cheap: the same code base, but only one of the two machines may start runs, spend money and act outside.

Part II

How it got here

The project is older than its first commit. It starts on 4 June with documents and writes its first line of code on 14 July. The 1,097 commits since then have two peaks and one conspicuous dip, and the commit titles tell a story that nobody plans: first features were built, then guards, and then it turned out that a good number of the guards were dead.

3002001000 23332939624722919157 13 Jul20 Jul27 Jul3 Aug10 Aug17 Aug24 Aug31 Aug commits per week, weeks start Monday, total 1,097, last bar is the current week up to 6 Sep
Figure 4. The dip to 19 commits in the week of 24 August is not a pause in the project but a pause before the audit. The week after it began with two guards that had shot the messenger, and the current week, in amber, is the one where the numbers turned on the tool itself.
4 Jun to 22 JunBefore the code

Six weeks in which not a single line ran

On 4 June four strategy documents for seitenhebel appear: an executive summary, customer segments, a funnel psychology audit and a money-back guarantee. The first one notes about itself that it was synthesised “during the gym session” from ten parallel deep analyses. Two days later a competitor pricing analysis joins them.

The directory itself is created on 20 June at 15:56. The next day, in a single day, eleven documents appear: architecture, strategy loop, operating model, premortem, quality and moat, tool options, go-to-market motions, partnerships, market intelligence, an interface map to my video tool and an architecture diagram. Two more follow on 22 June.

Together that is 18,773 words in 18 documents, written before anything ran. That they only show up in git on 14 July is a property of how they were imported, not of when they were written: they came in as a batch through one pull request.

Why the commit curve does not show thisA git log measures when text was versioned, not when it was thought. Between the first document and the first commit lie 40 days, and the architecture documents from back then describe a ports-and-adapters structure that the code later never adopted.
14 JulFirst line of code

The impulses come from the agents

Two commits on one day set everything up: a strategy foundation of documents and contexts, and ten agent loops with one generic runner. The guiding principle stands in the header comment of the runner and has survived 54 days: every loop ends in an artifact and a notification with one concrete action. Sending, money, captchas and logins are always the human’s.

1c4e5653MarketingOps: project foundation. Strategy docs, contexts, browser automation, growth runbooks
e21ba064MarketingOps: loop operating model. 10 agent loops, scheduled, human-in-the-loop gated
15 to 17 JulWeek 1

From a sticky note to a backbone

On the second day the system gets a face: the cockpit with a “morning edition”. Two days later comes the decision that carries everything after it: tickets instead of a queue that is gone after a restart. The same day the deal CRM is born, and the outreach loop is shut down before it ever ran. Cold outreach is legally fraught in German-speaking markets, and the strategy does not carry it.

51f67768Ticket system: a durable backbone instead of a volatile queue
a302ff38Sales agent: deal CRM, follow-up engine, weekly report
0ef5a642Cold outreach out of the strategy: outreach loop shut down
26 Jul to 6 Aug380 commits

“Done” means live. Not: a file is lying around

The most productive phase of the project, and the one in which the central insight lands. The cockpit gets four exits instead of one dead end. The proposal valve appears. The rank of a proposal is computed instead of guessed. And then the measurement that the whole reconcile idea rests on: at that point the chain had never closed a single card.

9ef95a51The baseline is in: 4.2 %, and the chain has never closed a card
6a397050“Done” means live. Not: a file is lying around
ac04dcedThe four silent failures had a reason. It just never reached the database

On 27 July the project also gets its own rules file, whose stated job is to “tell the agent where it stands”. That sentence is literal: workshop or production, and what it is allowed to do in that place.

6 to 16 Aug279 commits

The rules do not reach the executor

The phase in which the project stops building features and starts testing its own rules. The finding is uncomfortable and repeats in several shapes: a lock that was not one. Vocabularies that were not unused but bypassed. Three exception lists, each justified on its own and together invisible.

826de365The lock was not one: 327 lines of window between question and answer
e66912b2Four vocabularies were not unused, but bypassed
944010eeThree exception lists, each justified on its own, and together invisible
3f8602d4The brake against twitching did not engage on a single call
The formula that came out of itA rule does not work because it is written down. It works only where the executor is looking. On 7 August a permission failed on exactly that: “then we make an exception for swiftrun too” stood in the cockpit chat and had no effect, because the rules file knew nothing about it.
22 to 24 AugThe turning point

The guard caught me first

The densest day in the project’s history. The handover stops being a shared file and becomes a journal of individual threads, because two parallel sessions had overwritten each other. The rules file stops being its own logbook: 56 closed openings move to an archive. The Buffer button appears, through which a LinkedIn draft finds its way out. And the re-measurement on the real machine finds two errors nobody had seen before.

8b707e34Handover as a journal: the Buffer button thread, and the backlog rescued
f442da1bOpening: deploy the Buffer button, and the guard caught me first
a952aa43Opening closed: the Buffer button is on production, and the re-measurement found two errors
49a64d4eThe self-attack on its own specification found eleven weaknesses. One of them would have disarmed the blocklist

Right after that come the test bench, “the automated verification the loops were missing”, and a critic with its own context. And the card learns a distinction it had blurred before:

52d42c5dThe card showed a guess with the label of a fact
596cda68The baseline measured the transmission path instead of reality
2 to 4 Sep50 commits

The dead

After the quiet week comes an audit over roughly 21,000 lines of production code, and what it finds is the punchline of the whole chronicle: not broken features, but guardrails that had done nothing for weeks. An escaping routine that had been ineffective for 17 days. Two guards that shot the messenger instead of reporting an error. A loop that stood for three weeks in a directory without its dependencies and therefore structurally could not produce anything. Ten symlinks pointing to macOS paths that led nowhere on the Linux production machine, unnoticed since 24 March, because the test only checked them on the Mac.

d27573e3The apostrophe escaping in the cockpit did not work for 17 days
e8466ecaTwo guards that shot the messenger: identity check and chat return path
541b425cThe dead tree: why no video was produced for three weeks
960e1582The card names its site. Nobody read it
51b7e9a3The button that never existed
Why this is not a setbackEach of these findings was found because somebody measured on the real machine instead of the development machine. The symlink error is the purest case: the same test is green on the Mac in both states, before and after the repair. It only turns red where it counts. I wrote up the worst of them in the watchdog that died quietly.
6 SepThis week, 157 commits

The numbers turned on the tool

This morning I ran the query I had been avoiding. Thirty days of production, everything a human did at the cockpit, next to everything that was done to the tool in the same period.

A read-only SQLite session showing thirty days of human actions at the cockpit, and acceptance day by day: five cards on 22 August, none for fifteen days, thirteen on 6 September
Run against the production database this afternoon. The middle block is the one that matters. Five cards accepted on 22 August, then fifteen days without a single row, then thirteen this morning between 08:07 and 11:27.

It was never a usage problem, and that is what made it serious. I kept clicking. I cleared cards, I sent cards back, I left comments: 259 of them in thirty days. What had stopped was acceptance. Five cards went to done on 22 August, and then, for fifteen days, not one. In those same fifteen days, 273 commits went into the tool. As long as that ratio stands, every further guard improves a machine nobody takes work from.

So the machine room got a build freeze this morning. Until 20 September nothing gets built that is not one of three moves: switch off loops that have no exit, build exits that need no click, and let stale cards expire. Every commit in that window has to name which of the three it serves.

Then I ran the query again this afternoon, for this post. The number had moved. Between 08:07 and 11:27 today, thirteen cards went to done by hand, the first acceptance in fifteen days, and the commit count into the tool for the same day is zero. One morning is not a trend and I am not going to pretend it is. But it is the first data point that points the right way, and it arrived on the day the building stopped.

On 20 September two numbers get read: cards a human took to done since 6 September, and articles published across my sites, today 413 and last increased on 17 August. If both rise, the exit was the problem and the pause worked. If nothing rises, it was not the building either, and the next question is a different one from “which guard next”.

What the numbers say about the machine room

In the directory where the machine lives, test code is slightly longer than production code, a ratio of 1.01 to 1. That is not a style decision. It is the result of the chronicle above. In a system whose agents run unsupervised at night and whose reports nobody reads live, a claim without a measurement is worthless. That is why a card does not close on say-so, why a permission needs a committed paragraph, and why behind almost every rule in the rules file stands a date and a case.

What 94 days actually bought

Counted from the first document that is 94 days, from the first commit 54. The real development of this project is not that it can do more. It is that it stopped believing its own success reports. And the honest end of this post is that a machine that distrusts itself is still a machine nobody has accepted work from in fifteen days. The next fourteen days are about that, and the measurement is already on the calendar.

All numbers were measured from the repository on 6 September 2026, not estimated: commit counts from git log on the directory, loops and schedules from the loop configuration, tables from the database module, states from the transition table in the ticket module, proof types from their config file, lines with wc -l without node_modules. The prehistory comes from the creation lines of the documents themselves and the directory’s birth time. Commit titles are translated from German; the hashes are real and the originals are one git show away. The ledger of human state changes was measured on the production database on 6 September.

Georg Singer

Georg Singer

I automate my own projects in public and write down what each one cost. Physics degree, doctorate in computer science, nine systems in production on hardware I own, all of it built in my own time.

More from the log