Skip to Content
Data · Input formats

The people export, sealed before it is stored.

The people export maps each person to a team, and each team to a cellCellA box on the map that stands for a team, service, supplier or AI agent. It is a unit you can inspect, not necessarily a department or a person. in the mapMapThe view of teams, services and their dependencies for the selected part of the organisation. Details appear beside the map.. It feeds the sealed identity joinSealed identity joinStored links between employee fingerprints and teams or services. The fingerprints replace readable employee identifiers. Product pages do not show the links for individual people., which the key-person check reads. The join stores hashesHashA fixed-length fingerprint calculated from content. Comparing fingerprints helps detect changes and match records. instead of readable employee ids and email addresses. No page in NovusLens shows a headcount.

The key-person check looks for a cell that sits on a measured critical pathCritical pathThe dependency chain with the greatest total coordination weight in the map. It identifies a structural constraint. It does not state elapsed time. and has one to four people mapped to it. Such a cell is named as a small team, without a number. A critical path is the chain of dependenciesDependencyA link where one unit needs something from another. A dependency is not a reporting line: it shows where work relies on something else. with the greatest total coordination weight. It identifies a structural constraint, rather than elapsed time. Its measured weights come from observed work timings.

Hashed on the way in, read only as a count per cell, and surfaced as a small-team alert with no count and no names.

What the export carries

The export lists HRHRHuman resources: the organisation's people records and the team that maintains them. teams, their map cells and the people in each team. It also states whether membership is complete for the listed teams. A person is identified by an employee id and an email address. Both are hashed on the way in.

Person-level data never enters the org chart. The org chart carries team-level units only. See the org chart format.

What is stored

Before anything is written, the connectorConnectorSoftware that reads records from a source system, such as an incident tool. A customer-written file connects those records to teams or services in NovusLens. hashes two values for each person:

  • The employee id, as a SHA-256SHA-256A method that turns content into a fixed-length fingerprint. Comparing fingerprints helps check whether content changed. hash with the identity saltIdentity saltA secret value added before employee identifiers are turned into fingerprints. Keep the same value so later imports can still match existing people. added. The connector trims and lower-cases the value first.
  • The email address, as a SHA-256 hash with the identity salt added, prepared the same way.

The clear values are never written. Each stored person has a team name and cell id. The key-person check can count people per cell without reading their identities.

Each person’s row is kept for 180 days after the last export that named them. Each file that names a person resets that clock. A person a complete snapshot withdraws keeps their row for the audit, but no read sees them again.

Docker commands

For the Compose installation, run commands from the checkout that holds .novuslens.env and compose.yml. Prepare the input described below, then import it with ./lens, which uploads the file into the running reasoning containerContainerA running program with its own packaged software and isolated environment, managed by Docker on the host.:

./lens connectors people --org harbour --fixture people.json

Use your organisation id and the chosen account wherever a command asks for them. The installer sets the identity salt in .novuslens.env; preserve it with the installation.

Follow the complete Operations manual for accounts, configuration and command access.

Before you start

  • Export the people list from the HR system into the shape below, and save it as a .json file.
  • Make sure each cell id in teams is the id of a cell that is already in the map. The org chart creates the team cells, so import it first.
  • Run the command from the installation directory. ./lens runs it in the installation’s reasoning service, with the installation’s configuration. It first copies any local file named by --fixture, --file, --mapping or --enrolment into the service.

The JSON shape

Top level

FieldRequiredWhat it does
teamsYesA list of {team, cell} pairs. team is the HR team name. cell is the cell id in the map.
peopleYesA list of people. Each person needs employee_id, email and team.
export_idRequired when complete is trueThe name of this export. It is recorded against every person it places.
completeNotrue declares that this file is the whole current membership of the teams it lists. Omit it, or set it to false, for an incremental file. Any other value is refused.

Each person

FieldRequiredWhat it does
employee_idYesThe HR employee id. Hashed before it is stored.
emailYesThe work email address. Hashed before it is stored.
teamYesThe HR team name. It must appear in teams.

A person whose team is not in teams is reported as an unknown team, and skipped. A person missing one of the three required fields is reported as malformed, and skipped. An unknown-team person does not count as a leaver. A malformed person is different: In a complete snapshot, a malformed person already on the map is withdrawn. They are not counted in the new membership list.

Worked example

Snapshot for September, covering two teams:

{ "export_id": "hris-2026-09", "complete": true, "teams": [ {"team": "Bookings Desk", "cell": "bookings"}, {"team": "Customs Filing", "cell": "customs"} ], "people": [ {"employee_id": "E1001", "email": "a.person@example.org", "team": "Bookings Desk"}, {"employee_id": "E1002", "email": "b.person@example.org", "team": "Customs Filing"}, {"employee_id": "E1003", "email": "c.person@example.org", "team": "Customs Filing"} ] }

In October, E1002 has left, E1003 has moved to the bookings desk, and E1004 has joined customs filing. Sent as a complete snapshot again:

{ "export_id": "hris-2026-10", "complete": true, "teams": [ {"team": "Bookings Desk", "cell": "bookings"}, {"team": "Customs Filing", "cell": "customs"} ], "people": [ {"employee_id": "E1001", "email": "a.person@example.org", "team": "Bookings Desk"}, {"employee_id": "E1003", "email": "c.person@example.org", "team": "Bookings Desk"}, {"employee_id": "E1004", "email": "d.person@example.org", "team": "Customs Filing"} ] }

After October’s file, E1002 is withdrawn, E1003 is attributed to bookings, and E1004 is attributed to customs. Had October’s file been sent without complete, E1002 would still be attributed to customs until the 180-day limit ran out.

The repository also holds Meridian Grid fixturesFixtureA prepared set of records used for a test or demonstration. A fixture is sample data, not a live customer's records., meridian_people.json and meridian_domain_taxonomy.json in apps/connectors/fixtures. Meridian Grid is a fictional demonstration organisation. Its fixtures are not a real export.

How to send it

From the installation directory:

./lens connectors people --org harbour --fixture people.json
  • --org is a top-level option, so it comes before the subcommand name people.
  • --fixture is the path to the export file.
  • --host and --port default to the meshMeshA set of connected Mycelium services that exchange updates and keep the NovusLens log. address in the environment, as described on the org chart format.

The people export has no browser import. Send it from the command line only.

What a re-run does

Illustrative: a complete snapshot moves one person, adds one and withdraws one; an incremental file would leave the absent person in place.

An export does not replace the previous one by name. It updates people one at a time. What happens to a person the file does not mention depends on complete.

SituationIncremental (default)Complete snapshot ("complete": true)
A person in the file, not seen before (joiner)Added to their team’s cellAdded to their team’s cell
A person in the file with a new team (mover)Moved; the latest team winsMoved; the latest team wins
A person in the file, unchangedRefreshed; one row per person, never twoRefreshed; one row per person, never two
A person not in the file, in a team the file lists (leaver)Untouched until the retention limitWithdrawn now: no read sees them again, the row stays for the audit
A person in a team the file does not listUntouchedUntouched: a snapshot covers only the teams it lists
The same file twiceNo changeNo change; nothing withdrawn

A monthly HR export should be sent with "complete": true and the full list of teams it covers. A file that covers one team, or one department, is still safe to declare complete, because only the teams in its teams list are affected. A file sent without complete can never remove anyone.

Two behaviours the table does not state:

  • A person a file names again is brought back if an earlier snapshot had withdrawn them. The summary reports how many people this file brought back.
  • A withdrawn person’s row is not erased. The withdrawalWithdrawalA source’s removal of a value or record from current use. Its history stays, and NovusLens checks conclusions that depended on it again. only ends their retention, so no read sees them.

What you will see

On success the command exits without error and prints one summary line. The line states which case ran. A complete snapshot prints:

people: 3 joined across 2 teams — sealed (hashes only, floored aggregates, 180d retention); complete snapshot 'hris-2026-10': 1 withdrawn (in the listed teams, not in this file)

An incremental file prints the same opening, followed by incremental (adds and moves only — nobody removed; declare "complete": true for a snapshot). The figures above belong to the illustrative example, not to a real run.

Any skipped person is added to the line as SKIPPED n (surfaced), followed by the reason for each skipped person: unknown-team with the team name, or malformed-person with the missing fields. The map shows the result after the projectionProjectionThe regular pass that brings the read model up to date with the canonical log. It runs every 30 seconds by default. service’s next pass, about 30 seconds by default.

The key-person check

The check is a separate command. It is not run automatically after an import, so run it when you want the result:

./lens graph keyperson --org harbour

The check raises a findingFindingA difference in the records that needs a decision. It includes its sources and can describe a slow handoff, disputed fact or missing owner. for each cell that is on the measured critical path and has between one and four people mapped to it. The finding says the cell is a small team on a key dependency path. It does not state a count. The final line reads key-person risk: N finding(s) surfaced. A note explains that this is an alert rather than advice. Identities and counts stay hidden.

The check reads a count of people per cell from the sealed join. It does not read any identity. A finding is not evidence that the person is essential to the work. It shows only that the mapping has few people in a cell that carries a measured critical path.

When something goes wrong

A refusal stops the command before anything is written. The command prints a Python traceback and exits with a non-zero status. The reason is the last line of the traceback. The next line names a file with the full traceback. That file can hold secrets: see Configuration. These are the refusals:

MessageCause
`complete` must be true or falsecomplete holds any value other than true or false.
a complete export must carry an export_id (the name that identifies this snapshot)complete is true and export_id is missing or blank.
a complete export with no people would withdraw every listed team; refused — send it as an increment, or list the peoplecomplete is true and people is empty.
export_id '<id>' was already applied with different content; give this export its own idThe same export_id was used for a file with different content. Sending the same file again under the same id is accepted.

A malformed person or an unknown team does not stop the command. Each is skipped and reported on the summary line, as described above. Fix the export and send it again.

Last updated