Unify Your Plant-Floor Data with Claude Code and TimescaleDB

Wait 5 sec.

A typical plant floor speaks many protocols. Three of the most popular are Modbus/TCP, OPC UA, and MQTT. None of these define how a tag is named or how a timestamp is recorded. One source might stamp readings in local time; the next in UTC. One device uses snake_case, the other kebab-case. Each mismatch fragments the Unified Namespace (UNS) you set out to build. Closing those gaps by hand is the usual answer.This article walks through a working template that uses Claude Code Agent Teams to build one collection client per protocol, each writing directly into a single governed TimescaleDB table with UNS naming enforced as the data lands. The reasoning behind each piece is what lets you adapt it to your plant and your operators’ know-how.What a Unified Namespace Actually CostsThe namespace itself is the cheap part. What costs you is closing the gap between many protocol-specific sources and one consistent dataset, and keeping it closed as the plant floor changes.There are two conventional ways to close that gap. One is to buy it: a middleware or broker product that ingests every protocol, normalizes the names, and forwards a clean stream. It works, and it is another vendor in the stack, another license, another box to operate. The other is to do it by hand: engineers reconcile device configurations and tag-mapping files until the names line up. That works too, until someone adds a tag and the reconciliation starts over. Neither option makes the consistency self-enforcing. The naming contract lives in someone’s head or someone’s spreadsheet, and it drifts.There is a third option: build the collection layer yourself, but implement it as a coordination problem rather than a coding marathon. One small client per protocol, each reading from its source and writing directly into a single governed TimescaleDB table, with the UNS naming contract enforced while the code is written instead of reconciled after. Claude Code Agent Teams make that practical: a Lead session and several teammate sessions that share a task list and a peer mailbox, enough to build multiple clients in parallel against one contract without the work drifting apart.The contract is the coordination layer. Agents enforce it; they do not replace the engineering judgment that wrote it.Agent Team in Action: Three Clients, One ContractIn this section, the agent team builds three collection clients against one shared contract that holds them to the same output, reviews the resulting code, and verifies the readings they write to TimescaleDB. The companion repository has the scaffold, the prerequisites, and a one-command setup script if you want to follow along.Five agents do the work:| Agent | Type | Role ||----|----|----|| modbus-client | Teammate | Poll a Modbus-TCP device and write readings to TimescaleDB || opcua-client | Teammate | Subscribe to an OPC UA server and write readings to TimescaleDB || mqtt-client | Teammate | Subscribe to an MQTT broker and write readings to TimescaleDB || contract-reviewer | Read-only reviewer | Static review of all three clients against CLAUDE.md || timescale-validator | Validator | End-to-end pipeline check against landed data |The three client teammates are not equal in design. modbus-client and opcua-client poll their sources on an interval; mqtt-client consumes a live broker stream, where batching and back-pressure genuinely matter.On disk, the whole team is a handful of Markdown files and a few scripts:├── CLAUDE.md # The UNS data contract (binding for all clients)├── .claude/│ ├── settings.json # Agent Teams enabled + teammate permissions│ ├── agents/│ │ ├── modbus-client.md # Modbus-TCP client teammate│ │ ├── opcua-client.md # OPC UA client teammate│ │ ├── mqtt-client.md # MQTT client teammate│ │ ├── contract-reviewer.md # Cross-client reviewer definition│ │ └── timescale-validator.md # End-to-end pipeline + DB validation│ ├── rules/│ │ ├── shared-code.md # Import from shared/, never redefine│ │ ├── file-ownership.md # Which agent writes where│ │ ├── python-conventions.md # uv, naming, imports│ │ └── definition-of-done.md # Client completion criteria│ └── skills/│ ├── run-tests/SKILL.md # Run client unit tests│ ├── validate-contract/SKILL.md # Static review against CLAUDE.md│ └── validate-pipeline/SKILL.md # End-to-end pipeline validationThere is no orchestrator agent here. Your main Claude Code session plays the Lead, and you are the audit point that closes the loop.Two walkthroughs follow: a happy path through clean code and clean data, then a deliberately broken run that shows what AI reasoning under ambiguity looks like in practice.The happy pathWhen you kick off the team, three teammates start working in parallel. Each has its own scope (clients/modbus/, clients/opcua/, clients/mqtt/), reads the contract on startup, and stays out of the others’ files.Once the three clients are built, the pipeline runs through two validation layers. First, code review: the contract-reviewer reads each client’s source against the UNS data contract, flagging naming violations, hierarchy mismatches, and timestamp handling that doesn’t match it. It is read-only; it judges, it doesn’t fix. Then, data review: the timescale-validator runs the deterministic validate-pipeline skill, six checks against the readings that landed in TimescaleDB, and interprets the result.Two layers, two intentionally separate concerns: the reviewer catches what the code says, the validator catches what the data does.To verify results, open the Tiger Cloud SQL editor and ask the namespace itself:-- Full UNS namespace: six-level ISA-95 hierarchy materialized in PostgresSELECT enterprise, site, area, line, cell, tag_name, uns_pathFROM uns_namespaceORDER BY uns_path;Every reading the three clients wrote lands keyed to a row here: one namespace, assembled from three protocols.That table, queryable as a single object alongside tag_history, is the payoff.The failure pathSix checks passing on a fresh run is a useful sanity test, not a demonstration that the framework has teeth. Stay in the same session, open the Tiger Cloud SQL editor, and break something downstream that your agents can’t see:-- Shift area_02 timestamps back 5 hours-- (simulates naive local time written as UTC)UPDATE tag_historySET ts = ts - INTERVAL '5 hours'WHERE tag_id IN ( SELECT id FROM uns_namespace WHERE area = 'area_02');That UPDATE simulates one of the most common failure modes on a real plant floor: a single source quietly stamping local time as UTC, exactly the case a UNS validator should not just catch but explain. Re-run /validate-pipeline in the main session. The deterministic half does its job: cross-area drift fires, area_02 lands five hours behind area_01.Interpretation is the other half. From where the Lead stands, the picture looks tame: code review passed minutes ago, configurations are unchanged, only area_02 readings look shifted. Your agents only know what their prompts taught them. The excerpts below are verbatim from our Lead run; your run will read differently, but the gap in the reasoning will be the same.“The earlier timescale-validator report was wrong. It claimed”all 6 checks PASS. cross-area drift PASS, aligned within ~1s.”The Lead then goes looking for the real cause:“The pipeline itself is actually sound. I just ran all three clients fresh against the live simulator: every cell - including both area02 cells - Landed at correct, aligned UTC (15:14). The OPC UA client log confirms it correctly handles SourceTimestamp=None by falling back to ServerTimestamp (timezone-aware UTC); Modbus stamps datetime.now(timezone.utc); MQTT decodes tsms with tz=timezone.utc. The code is git-pristine and […]”Read that diagnosis carefully. It is internally consistent. The code passes review, the bug looks historical, the explanation closes. It is also wrong. Look at what is missing from the diagnosis: any check against the database itself. We edited the data directly, the client code is fine, and the actual cause is an UPDATE the Lead never queried for.That gap is the lesson. The Lead didn’t query tag_history.ts against the wall clock or check for recent writes against the tag_history table. It took the path of least resistance through an under-specified prompt: code passed, the simulator looked clean, the failure must be historical. A plant engineer who’d been burned once by an out-of-band schema change would add a single behavior rule to the validator’s prompt: “When a deterministic check fails, query tag_history.ts against wall clock and review recent writes before concluding cause.” That one line, encoded in the agent’s prompt, would have caught what this run missed.Where You Inject Know-HowWhere does that prompt-level know-how actually live? Two seams hold it, and you've already seen both in the team's file tree: a shared layer that every agent inherits on startup, and a per-agent layer where each worker's scope and behavior live.Shared layer: the contract and rulesEvery agent reads CLAUDE.md and the files in .claude/rules/ before doing anything else. Anything you put there applies to the whole team, every session, and survives when the team turns over.CLAUDE.md carries the contract: ISA-95 hierarchy, naming rules, timestamp handling, and the table schema each client writes to. A short excerpt from the timestamp section:1.All timestamps MUST be stored as UTC. `tag_history.ts` is TIMESTAMPTZ; write timezone-aware values only.2.If the source provides naive timestamps (no timezone info), the client MUST: -Convert using `source_timezone` from `config/{client}.yaml` -Log a warning: "Naive timestamp from {source}, assuming {timezone}" -Never silently assume UTC.3.Clients that stamp at read time must document the clock source.That’s the template. What goes here from your side is everything the contract doesn’t yet anticipate: allow-listed legacy tag formats, per-area timezone overrides, a sensor model that reports values in centi-units instead of base units. Each one is a paragraph away from being enforced across every client.The other half of the shared layer is .claude/rules/: separate files that apply with the same weight as CLAUDE.md. The split is about organization, not priority. In this tutorial we used rules for team-wide conventions, like definition-of-done.md (when a client is complete: contract passes, tests pass, no hardcoded values), file-ownership.md (which keeps two client teammates from corrupting shared code), plus Python and shared-import conventions. On a more complex plant floor, this same folder is where you would push per-area or per-sensor specifics to keep CLAUDE.md itself lean. Add a rule when you find yourself reminding every engineer of the same thing every time.Per-agent layer: roles and toolsThe shared layer covers what’s universal. The agent layer is where each agent diverges: its scope (which files it may write to), its behavior rules (how it reasons through its task), and the deterministic skill it invokes first. Here’s the behavior section of timescale-validator.md:## Behavior Rules-Run the validation first. Interpret, don't repeat raw output.-Ground every insight in data: query results, row counts, timestamps.-When diagnosing a failure, cross-reference client code and config.-Distinguish testing/prototyping-scope findings from production recommendations.-When done, message the Lead with the full validation report.That bullet list is where the failure path most directly cashes in. The rule a plant engineer would have added, "When a deterministic check fails, query tag_history.ts against wall clock and review recent writes before concluding cause," has more than one home, and the choice is yours:As a shared rule in CLAUDE.md or .claude/rules/, when the diagnosing session is your main lead, and you want every agent to inherit it on startup.As a behavior bullet, a sixth line in timescale-validator.md, when you delegate diagnosis to that one agent.As deterministic code, a check in the validate-pipeline skill that flags any area whose latest timestamp lags the others by a suspicious round-number offset, when you'd rather the script catch the pattern than trust an agent to remember.Same know-how, three encodings: prose every agent reads, prose one agent reads, or Python nobody has to remember. Pick whichever your team will keep maintained.That third option is the hybrid principle paying off: determinism, where you can, interpretation where you can't. Skills carry the deterministic half; Python, an agent invokes rather than reasons through. The pipeline's validation logic lives in scripts/validate_pipeline.py; validate-pipeline is the prompt-level wrapper that invokes it. We keep Python in scripts/ rather than bundling it in the skill folder, but both approaches work; pick whichever you prefer.This is our opinionated cut of the framework, not a prescription. Swap, narrow, or expand any of the primitives. There is no one-size-fits-all UNS pipeline because there is no one-size-fits-all factory.From Clients to Governed DataThe Agent Teams produce three clients that write straight into TimescaleDB. No broker in the middle, no ingestion service to operate, no second copy of the data to keep in sync. The namespace and the readings sit together in one Postgres instance, which is what turns contextual questions, and AI-agent queries, into a single JOIN instead of a cross-system integration project. It is also what lets the data validator close the loop on the code reviewer.That same architecture is a starting point, not an endpoint. Scheduled drift reviews, on-demand questions against the namespace, real-time anomaly checks: each one would be a different shape of the team you just ran, written by the engineers who already know which drift matters and which anomaly is noise. One-shot build today, 24/7 operational layer next.Clone the repo, point it at a Tiger Cloud instance, and start replacing the slim agent prompts with the bug your team learned from last quarter. The template is the floor. The ceiling is the know-how your engineers already carry.