footie / Football-content automation / 2026
Connecting football posts to the searches that found them.
Thirteen n8n workflows collect, normalize, rank and draft football material, with separate query-hit records to retain discovery context alongside the post.
- ROLE
- Software engineering
- STATE OF THE BUILD
- Prototype
- RECORD UPDATED
THE WORK
The project.
footie takes collected football posts through normalization, ranking, model-assisted drafting and a style check before a Telegram handoff. Its data model separates what a post says from how it was found: raw and normalized records use the post ID, while query hits pair that ID with the search query. Later stages can use discovery context without storing another full copy of the text for every match.
That distinction runs through the workflow design, but one early filter currently works against it. The normalizer skips repeated post IDs before constructing query-hit rows, so a second match in the same batch can disappear. The exported implementation makes both the intended relationship and the correction needed visible; this account doesn't establish current scheduler operation or a completed drafting run.
IMPLEMENTATION
Design choices.
- Posts and discoveries have different identities. One post can belong to several query results without duplicating its text. To preserve that relationship within a batch, the early seenIds filter needs to stop discarding the second hit before it reaches the compound key.
- Refresh timestamps advance before dispatch. This keeps the next scheduler tick from immediately selecting the same refresh, but a failed branch can leave a recent timestamp without completed data. Recovery needs to account for that difference.
- Optional text-hash deduplication is off by default. Its normalization strips links, mentions, hashtags and punctuation, which can make different original contexts look the same. The hash is a content filter, not a security check.
- Each representation uses a stable-key upsert. Replaying a write can update its existing row, but the three writes don't share a transaction. A raw post can exist while its normalized form or query history is still missing.
- The static-data lock supplies an expiring orchestrator guard. The export doesn't establish that it behaves as a distributed lock across every possible n8n deployment.
HOW IT FITS TOGETHER
Architecture.
- WF-00 decides which collections need refreshing using intervals from a control table. It holds an expiring lockUntil value in workflow static data and advances the selected refresh timestamps before dispatching asynchronous branches.
- Query and polling workflows feed WF-30 with post text, metadata and discovery context. JavaScript normalizes the text, applies batch filters and creates separate row sets for raw posts, normalized posts and query hits.
- Raw and normalized records are upserted by post_id; hits use post_id plus query_id. The three Data Table writes are independent, allowing existing rows to be updated on replay but leaving partial completion to reconcile.
- Ranking and model-assisted drafting use the collected material, followed by a style check and Telegram handoff. The query-hit relation supplies the discovery context separately from the normalized text that those stages consume.
Explore the architecture map9 components · 10 connections
Scheduled collection and three-table normalization
A controller starts collection; accepted posts fan out into content and provenance stores before drafting.
Control and collection
Content processing
Records and delivery
Scroll or drag the background to move. Use the zoom buttons to resize.Arrow keys move between components. Enter selects.
Choose a component to explore
Select a numbered component on the map or use the component menu. Its details and connections will appear here.
No component selected.
All connections (10)
- 01 · Scheduled controller → 02 · Query and polling workflowsDispatch collection · Async task
- 02 · Query and polling workflows → 03 · NormalizerCollected items · Direct call
- 03 · Normalizer → 04 · Raw postsUpsert · Async task
- 03 · Normalizer → 05 · Normalized postsUpsert · Async task
- 03 · Normalizer → 06 · Query hitsUpsert · Async task
- 03 · Normalizer → 07 · Candidate rankingContinue content path · Direct call
- 05 · Normalized posts → 07 · Candidate rankingNormalized material · Direct call
- 07 · Candidate ranking → 08 · Draft and style guardSelected candidates · Direct call
- 08 · Draft and style guard → 09 · Telegram handoffPrepared draft · Async task
- 09 · Telegram handoff → 01 · Scheduled controllerRun completion / lock release · Response / return
MADE WITH
n8n · Data Tables · JavaScript · OpenAI · Grok · Structured outputs · Telegram
CHECKS & RESULTS
What was checked.
A source review shows what the code does. It is not a fresh test of the running app.
Code and development records reviewedReviewed
The code, tests and available development records were checked. Reported test results keep their original scope. No fresh app or live deployment check was run for this write-up.
- Scope of this check
- Code review, not a fresh run of the app
NEXT
Still to work through.
The first regression I'd add is small: one post returned under two queries should produce one raw row, one normalized row and two hits. Then I'd interrupt each write and check that a retry restores the missing records. Those tests would exercise the data model and its recovery together; neither was run against the workflow runtime for this review.