Upload a staff list as an interim directory
This page is for an administrator who does not yet have a system of record connected — no HR warehouse, no people view — but who can export a list of their people to a spreadsheet. You upload that file to Prism, and Prism uses it to work out which person each Jira account belongs to, and which person each GitHub login belongs to. The second of those is the harder one, and it is worth knowing why before you export anything.
It takes about fifteen minutes, and nothing leaves your network: the file goes from your machine into your own appliance, and Tetrate never sees it.
This is an interim arrangement, and Prism is built so that you can undo it in one click. A file somebody exported has no refresh path and no leaver process — a person who has left stays in it until you upload a newer file. When you connect a real directory, hand the role over and this file stops being consulted.
Read Take a source live first if you have not connected any source yet, and see Connect your Snowflake directory if you already have a warehouse view of your people — that is the better answer, and this page exists for the weeks before it.
Before you start: one chart value, and it is already set
This page opens by sending you to Admin → Context streams. The one switch this page depends on ships on since 0.10.0, so on a stock install there is nothing to do here. Check it only if your values file pins it:
mcps:
semanticMcp:
enabled: true # puts the semantic query tools on the agent. DEFAULT since 0.10.0
Be precise about what it does and does not affect, because it is easy to assume it is only for warehouses and equally easy to over-read it:
- The join this page is about does not need it. Working out which GitHub login and which Jira account belong to which person is done by the ingest job, which compiles the crosswalk straight from your uploaded rows in Prism's own database. That happens on the ordinary schedule whether or not the semantic MCP is on.
- Reading the roster itself does need it. The semantic query tools are the only ones that reach a designated source's statement, so questions about the directory — who is in a team, how many people a division has — have no tool to answer from without them. The self test's designated route line fails and names the switch.
sources.registry.enabled, which renders the page itself, defaults to true
from 0.10.0; set it only if your values file pins it to false. What the chart
needs
explains what each renders.
What this actually buys you
Prism can answer "how many pull requests were merged last month" from GitHub alone. It cannot answer "how many did this team merge" — because GitHub knows a login, Jira knows an account, and nothing tells Prism they are one person.
The uploaded roster is what tells it, and the column that does the work is the email address. Jira discloses one beside every assignee it can, your staff list has one beside each employee id, and Prism matches the two exactly and compiles a crosswalk from them.
What it does not buy you, stated plainly so that nobody reads a figure the wrong way:
- it is not a headcount. It is the size of a file somebody exported, on the day they exported it. Prism will say so rather than answer "how many people work here" from it;
- it resolves only people who are in it. Somebody who joined after the export resolves to nobody until you upload again;
- it says nothing about who did what. It is a roster, not an activity record.
That is the whole story for Jira. It is not the whole story for GitHub, and the difference is the one thing on this page worth reading twice.
How the address reaches GitHub
Jira hands Prism an address directly. GitHub hands it none — every GitHub record names a person by their login and by nothing else, and there is no email field on a pull request, a review or an issue anywhere in the mirror.
So Prism asks for one. Since 0.11 the ingest walks your organisation's member list and reads each member's published profile address, recording the address beside the login — and it is your roster's address that the pair attaches to. The chain that resolves a pull request to a person is:
your file says
E9921is[email protected]· GitHub says[email protected]is@ada-l· so@ada-l's pull requests areE9921's
The email column is therefore doing the work for GitHub too — but only where those two addresses are the same one, and that is a real condition rather than a formality:
- a member who has published no profile address resolves to nobody. About a quarter of them, on the one organisation where this has been measured: 18 of 25 sampled members disclosed an address. Do not plan on that ratio. That instance populates the field from its own directory sync; on an instance where publishing an address is left to each person, the rate is whatever your people happened to choose, and it can be near zero. Yours is a fact about your estate and you will read it off the self test;
- a member who published a different address resolves to nobody either — a personal address, an old domain, an alias your export does not carry. They are indistinguishable from the first group in the crosswalk, which counts only what resolved, and they are the group you can actually do something about: it is fixed by getting the published addresses and the export to agree on one. Look at the domain before the name. Two spellings of one person are rare; a whole organisation publishing on a domain the HR export does not use is common, and it takes the yield to zero rather than reducing it.
So plan for a resolution rate short of your headcount, and read it off the self test rather than assuming it.
Agent Router spend is a third case, and this file does not reach it at all. Spend is keyed by the Router's own user id, and Prism resolves that only through a directory carrying that id — never through an address. A staff-list export has no column for it, so per-person spend answers name the id and say in as many words that they cannot name a person, rather than guessing. Nothing on this page makes that better or worse; it is simply not what an uploaded roster does.
The sweep is on by default and there is nothing to switch on. What it does need is your GitHub credential carrying the Members: Read organization permission — required from 0.11 for exactly this — which is not yours to grant from this page: How Prism reads GitHub sets out that grant and what turning the sweep off instead costs you.
ingest.github.identityEmailDomains is not a prerequisite. Left empty —
the default — the sweep still records what your own organisation discloses about
its own members. Setting it narrows what is kept to the domains you name, which
is a reasonable thing to want and is not needed for any of this to work. It is
also not the same switch as the older commit-author harvest, which reads
addresses out of commit metadata, is off until you name domains, and does not
run at all while GitHub is authoritative on the ingest engine — the default
since 0.10.0. That one is not something to plan a roster around.
The route that needs nothing from GitHub
If your export carries a GitHub username column, declare it. It is a fact your system of record asserts rather than one Prism has to go and ask for, so it resolves the people who published no address, needs no permission and no two addresses to agree, and survives somebody changing their GitHub profile. Section 3 is the three blocks it takes.
The two routes add up rather than compete — a person resolved either way is resolved, and each covers some of what the other misses. Most HR systems do not issue GitHub usernames, though, so look before you plan on it.
The failure this page exists to prevent
None of the above fails loudly. The upload succeeds, the crosswalk compiles, the source reports healthy, and nothing anywhere is refused — what you have is a directory that resolves everybody on one source and nobody on another, and no part of the product will volunteer that.
Read the crosswalk line once the first run has been through. It is one line, it is the only place this state is visible, and the section says which reading sends you where.
None of this is a reason to delay the upload. Jira resolves either way, and adding the username column later is an ordinary column change: your source, its stored file and its organizational role are all kept.
Export the new column first, upload it, and then edit the document — in
that order, and the order is the whole trick. Prism re-reads the file it holds
under your new column list before it saves anything, so if that file already
carries the column, the edit costs you nothing at all: the source never leaves
enabled and your directory never stops resolving anybody. If it does not
carry the column, the edit is refused (a directory that is switched off resolves
nobody, Jira included) and you upload the fuller export first.
1. Export your people, and look at the header row
Prism reads CSV. Export whatever your HR system gives you — you will describe it to Prism rather than cutting it down to a shape we chose — and open it to read row one. Those header names are what you will be working with.
Two rules about the file itself:
- the first row must be the column names. A file that starts with data has its first person read as a header, and Prism refuses it;
- columns you do not describe are ignored. Your export almost certainly has more in it than you need. Leave them; Prism reads only what its manifest names, so a spreadsheet cannot introduce a column into your appliance.
Check that your "preferred name" columns are actually filled in. They are often optional in the HR system, and a real export is usually mixed — filled for some people and blank for others in the same file. A name read straight off a blank cell loads a roster that resolves those people perfectly and cannot name any of them, and nothing about the upload looks wrong.
The recipe handles the surname for you: it maps both the preferred and the
legal last-name column, and prints the preferred one where it is there and the
legal one where it is not. You do not have to choose. If your export carries
only one surname column, delete the other and shorten the sql: line on the
family_name field to name the one you kept — the recipe's own comment says
exactly where.
The first name is mapped from the preferred column alone, because in the exports seen so far it is filled for everybody. If yours is sparse too, give it the same treatment: the recipe says how, in the comment above the name columns.
Your export is also likely to carry leavers, because the standard extract is
named for both — Workday's is All Active and Terminated Workers. Keep the
Active Status column if you have one: with it, ordinary questions are about
current staff and a question can still ask about people who have left. Without
it, every question about "the team" silently counts leavers and no filter can
exclude them.
Save as CSV UTF-8, not "Unicode Text". A spreadsheet's "Unicode Text" export
is UTF-16, and Prism refuses it by name rather than reading it as nonsense.
An .xlsx workbook is refused too, and says so — Prism reads CSV only, on
purpose: a workbook has formulas and several sheets and there is no honest way
to guess which cells are the roster.
2. Register the source from the recipe
The roster is a recipe, not a source that is already there. Prism ships an example document describing what an HR export usually holds; you make your own copy of it, edit it to match your file, and that copy is what runs. Nothing is seeded, and no columns are demanded of you that you did not write down.
- Sign in to Prism as an administrator and open Admin → Context streams.
- Press Add a context stream and choose People file (CSV upload).
- Prism shows you the document. Edit it now — section 3 is what to change.
- Press Register. The row is created, switched off, with nothing uploaded.
There is no credential to enter and no address to reach: a file source is fed, not fetched. The person who uploads has already signed in, and that sign-in is the whole authorization story.
3. Describe your columns
This is the only step that takes any thought, and it is the step that makes the
roster yours rather than ours. Every column you want Prism to read gets an entry
under columns:, and the shipped example has one for everything an HR export
typically carries — so mostly you are deleting the ones your export does not
have and correcting the spellings of the ones it does.
The rule that matters most
A column you declare is a column you require. If the document names a column and the uploaded file does not carry it, Prism refuses the whole file and tells you exactly which names it could not find.
That is deliberate. The alternative is loading a column of nulls, which gives you a roster that looks loaded and correlates to nothing. So:
delete every column in the example that your export does not have. It is a worked example, not a form to fill in. A refusal here is loud, immediate and one edit away from fixed; a column of nulls is silent and you find out weeks later from a wrong answer.
name: is ours, from: is yours
Each entry has two names, and only one of them is yours to change:
- {name: email, # keep: the rest of the document refers to this
from: "Work Email", # change: what YOUR file's header row says
type: string, identity: email,
description: "the corporate address; what Jira's assignees join on"}
name:is what the column is called inside Prism. Leave these alone — the dataset, the metrics and the identity links all refer to them by name.from:is what the header row of your file calls it. Matching ignores case and surrounding spaces, soWork Email,work emailandWORK EMAILare the same name — butEmail - Workis a different one, and you must say so.description:is what that column means. Write it in your own words if the example's does not fit; it is read by the model when it answers, and by the next administrator.
The three Prism needs
Everything else is optional. These are not:
name: | Why it cannot be deleted |
|---|---|
employee_id | The key. The one identifier your system of record issues once and never reuses. An address is reassigned after a leaver and names are not unique, so this is what a person is |
email | The join. Jira discloses an address beside its assignees, and this column is the other end. Delete it and the roster correlates to nothing. It reaches GitHub too, but only indirectly and only partly — GitHub discloses no address on any record |
given_name / family_name | How an answer names somebody. given_name maps from your preferred first-name column. family_name is not a column of your file at all: it is computed from two, family_name_preferred and family_name_legal, and prints the preferred surname where your export has one and the legal one where it is blank. Keep both columns, or delete one and shorten that field's sql: to the other |
A GitHub username column, if your export has one
This is the route that needs nothing from GitHub, and it is the only one that resolves a person who has published no address on their GitHub profile. It is not in the shipped example, deliberately — a declared column is a required one, and almost no HR export carries this one, so shipping it would refuse almost every upload. Add it yourself if yours does.
It goes in three places, and all three or none. The document names your
columns twice on purpose — once under columns:, which is how your file is
read, and once under the dataset's fields:, which is how the model is told
what a column means — and a third entry under identity_links: is what says
whose username it is.
Only one of the three omissions is caught for you, so copy all three:
| If you leave out | What happens |
|---|---|
the identity_links: entry | The document registers and the column loads, and nothing resolves. The crosswalk never learns the column is a username, and the self test's github_login:declared count simply never appears |
the columns: entry | The worst of the three. The document registers and the upload succeeds — the file is read, and simply has no such column in it — and then every compile fails on a column that is not there. A failed compile empties the whole crosswalk, Jira and all, so one missing line in one list stops every per-person answer on the installation. The self test's identity crosswalk line goes red and says the crosswalk was emptied |
the fields: entry | Refused, by name, before anything is stored: `github_login` is not a field of `csv_people` |
The middle one is the dangerous one, and it is worth being blunt about. A column named in
fields:or in a link but missing fromcolumns:is not in the table, so every compile fails on it — and a failed compile does not degrade, it empties the crosswalk. Not the GitHub part of it: all of it. Jira stops resolving too, on an installation where nothing was wrong with Jira. The upload will have succeeded and the source will look healthy; theidentity crosswalkline on the self test is the only thing that says so. Copy all three blocks, or none of them.
# 1. beside the other `columns:`, so the file is read
- {name: github_login,
from: "GitHub Username", # what YOUR header row says
type: string, identity: github_login,
description: "the person's GitHub username, where your system of record holds one"}
# 2. beside the other dataset `fields:`, so the crosswalk can see it
- name: github_login
type: string
identity: github_login
hidden: true
ai_context: >-
The person's GitHub username. An identifier to resolve THROUGH, never a
value to answer with.
# 3. beside the other `identity_links:` at the foot of the document
- from: employee_id
to: github_login
via: csv_people
from_field: employee_id
to_field: github_login
coverage: declared
hidden: true is not decoration: a GitHub username identifies a person, so it
is something to resolve through and never something to group an answer by.
A partial column is fine here, and this is the one place on the page where
that is true. A blank github_login cell costs you that person's GitHub
resolution and nothing else — every other column of their row still works — so
a column that only half your organisation has is worth declaring. What is not
fine is the column being absent from the file while the document declares it:
that refuses the whole upload, like any other missing column.
The manager column, and the question it unlocks
If your export carries the manager's employee id, keep that column and the
reports_to link that goes with it. Together they are what lets a question be
scoped to a leader's whole organization — "everyone who reports to X", directly
or at any depth.
Two cautions, both of which have bitten real installs:
- key it on the manager's id, never on a manager's name. Some exports write
a name with the id inside it, like
Full Name (12345678). Reading one out of the other breaks the first time somebody's name contains a bracket, and two people with the same name resolve to each other. If your export has only a manager name, you do not have this column — delete it, and that question is not answerable from your roster until the export carries the id. - delete the column and the link together, or neither. A
reports_tolink pointing at a column that is not there is a document that will not register.
A flattened management chain is not a substitute. Columns like
Management Chain - Level 04 are useful and the example keeps one: they let an
answer group by a level of your org. But they are a grouping, not a path.
Prism never walks them, because one organization's level 4 is not another's and
a fixed level expires at your next reorganization. The reporting line is the
manager id, walked.
Export the leaders too, not only the people under them
If you are exporting one organization rather than the whole company — "everybody under the VP of Engineering" — then check whether that leader's own row is in the file, and add it if it is not.
A chain-level column carries the leader's name on every row of their org, so without their own row they are in the file thousands of times as a value and not once as a person. Asked about them by name, Prism finds nothing in the name columns and reports that they are not in the directory — while a grouping by the same name returns their entire organization. Both statements are true and together they read as a contradiction, and the first is the one a reader acts on.
It costs one row. Do it for every leader whose name somebody might type: the person whose org you exported, and anyone the question "by manager" is going to name.
Check which chain level is which
Chain-level columns are numbered from the top of the company, so which number holds the leader you care about depends on where they sit — and it is not the same for two customers, or for one customer after a reorganization. The recipe declares three adjacent levels because one of them will be useless to you and you cannot tell which until you look.
If your export is one leader's organization, that leader sits at a level of their own and every row carries the same value for it:
| What you see | What it is | What it answers |
|---|---|---|
| the level with one distinct value | the leader whose org you exported | "whose organization is this" — and nothing else |
| the level below it | that leader's direct reports, one value each | "by manager". This is the column you want |
| the level below that | the directs' own directs | the second depth of "their directs" |
A roster of the whole company reads the other way round: nothing is constant, and each level is just a finer grouping than the one above.
You do not have to work this out from the org chart. Upload first, then run the two chain-level questions on the verification screen — they print the distinct values and a count for each level. The one that comes back as a single row is the leader; the next one down is your managers. It takes about ten seconds and it is the difference between "by manager" returning a useful table and returning one row that looks like a finding.
Then keep the levels you will actually group by and delete the rest, with their questions — a declared column your file does not carry refuses the whole upload.
Worth keeping if you have them
| Column | What it buys |
|---|---|
active_status | Who is still here. The standard extract carries leavers — Workday's is named All Active and Terminated Workers — and this is the only column that tells them apart. With it, ordinary questions read current staff and a question can still ask about people who have left. Delete it and you must delete the is_active field and the default_filters line naming it too; the recipe says so beside each. Note it is declared as a string, not a boolean, on purpose: as a boolean, No would read as true and every leaver would count as active |
management_level | Whether somebody manages anybody, as a seniority band from your HR system's controlled list. Nothing else says it: a business title can read "Director" while the band says Individual Contributor, so without this column an "individual contributors only" question is answered by guessing from job titles. The values are yours — the list is configured on your own tenant, so read what your own file returns before you rely on it |
worker_type | The denominator. "23% of engineers opened no pull request" is a different claim depending on whether the 100% includes people who are not employees, and an answer that cannot say which will lose the room. Another controlled list whose values are yours: one real export spells its two Employee and Contingent Worker, and yours may differ |
job_title | What a question like "the developers who report to X" filters on. Titles are free text and rarely consistent, so it is worth a look at how yours are spelled before you rely on it |
job_family | The same question, answered from your HR system's controlled list instead of from free text — and the better instrument where you have one. Needs one extra step: see below |
manager_name | "by manager" at any depth — the grain most questions about managers actually want. A chain level answers it only for managers who sit at that level, and on a large organization most do not |
management_chain_level_NN and its id | Grouping by a level of the organization — and, at the right level, the answer to "by manager". See Check which chain level is which above; keep the id column beside the name wherever your export has it, because two leaders can share a display name |
Read your own seniority bands before you rely on them
management_level is a controlled list, but the list is configured on your
HR tenant, so Prism ships no set and assumes none. The recipe carries a
verification question that prints your own bands with a count beside each; run
it straight after your first upload and read the whole list. Two things to look
for, both of which change how a question about individual contributors must be
answered:
- Middle bands. One real export carries eight values, including a Manager band and a Supervisor band alongside the more obvious ones. Where those exist, "individual contributors" has to be answered by naming the individual-contributor band, never by excluding the senior ones — a filter written as "not a Director" lets every Manager and every Supervisor through, and the answer looks right.
- Rows with no band at all. In that same export about 9% carry none. A blank is not an individual contributor; it is a person whose band the file does not state. An answer should set those rows aside and say how many, not fold them onto either side.
worker_type is the same kind of column and deserves the same look: its values
are your HR system's, not ours.
If you keep job_family, say what its values mean
job_title is free text, so Prism answers "how many developers" by matching a
substring of it and telling you which substring it matched. A job family is
different: it is a closed list your HR system issues — SW Development,
Software Architecture, Quality Assurance — and matching a substring over a
closed list is the wrong instrument. It misses a family whose name does not
contain the word, and it cannot tell you whether the ones it caught are the ones
you meant.
So Prism will not guess. Asked "how many developers report to X" against an
undeclared job_family, it lists every family with a count beside it and gives
you a range — which is honest, and is not the one-line answer the question
deserved.
Declaring the set is a four-line edit and it is yours to make. In the
job_family field's ai_context, replace the paragraph that begins "NOT
DECLARED on this installation" with your own, in this shape:
a DEVELOPER is job_family in (SW Development, Software Architecture,
Early Career - Technical, Database Development). QUALITY ASSURANCE is
adjacent and is NOT counted as a developer unless the question asks for it.
Name the families exactly as your export spells them. Prism ships no list of its own and never will: whether Quality Assurance is a developer is a question about your organization, and a list we picked would be wrong somewhere and invisible everywhere. An answer will still name the families it counted, so a reader who disagrees argues with the definition rather than with the number.
You can change it later — it is a document edit, the same as any other column description, and it takes effect on the next question. It does not need a re-upload of the file.
Or the person asking can name the families in the question
The declared set is the default, not the only answer available. Someone can also say which families they mean as part of the question — "how many people in SW Development and Software Architecture merged a pull request last month" — and get a one-line answer without anybody editing a document.
That is not a way around declaring the set, and it does not replace it. It is there because one installation can have more than one legitimate answer at the same time: Quality Assurance probably belongs in developers for a security-training question and probably does not for a pull-request-throughput one, and a single declared list cannot hold both. The declaration is what an installation counts when nobody says otherwise; naming the families is how a person says otherwise, for one question.
Two things follow, and they are the reason this is worth knowing before you write your declaration:
- The answer names the families it used either way, so a reader can always see whether the figure came from your declared set or from the asker's. A disagreement stays a disagreement about the definition rather than becoming two numbers nobody can reconcile.
- Spelling still has to match your export. Prism matches families against the values your HR system issues, ignoring case, and it does not guess at near misses — a family named in a question that your export does not carry contributes nobody rather than approximately somebody. If people are asking with names your export does not use, that is a sign the declaration should carry them, not that the question should keep being re-typed.
The fifteen-minute path
If you would rather not re-type a document at all, do the reverse: rename the
header row of your export to match the from: values, and leave the recipe
alone apart from deleting the columns you do not have. One edit to row one of a
spreadsheet, saved as CSV UTF-8.
from: and not name: — the match is only ever between your header row and
from:. name: is what the column is called inside Prism and is never
compared with anything in your file, so renaming a header to email when the
document says from: "Work Email" matches nothing and the upload is refused.
Both routes work and neither is better. This one is fewer moving parts today;
editing from: instead survives your next export, because the export keeps its
own spellings and nobody has to remember to rename anything again.
4. Upload the file
- Open the source you registered in Admin → Context streams.
- On its File tab, choose your CSV and press Upload and test (Replace and test, once a file is stored).
Prism parses the file before it stores it. If it cannot be read, nothing is stored and any file you uploaded earlier is exactly where it was — one bad export cannot take your directory away.
If it is refused, the message names what was wrong: which column was missing, which row had the wrong number of cells, where the file stopped being valid CSV. It never quotes anybody's data back at you, so a refusal is safe to paste into a ticket.
5. Read what it found
A successful upload reports what is in the file. Read it before going further — this is the "is my data any good" check, and it is cheaper here than after a report has gone to your leadership.
2 rows read.
employee_id: every row has a value.
email: 1 of 2 rows have a value — 1 is empty.
given_name: every row has a value.
family_name_preferred: 1 of 2 rows have a value — 1 is empty.
family_name_legal: every row has a value.
What to look for:
| What it says | What it means |
|---|---|
| The row count | Should match the row count of your spreadsheet. If it is lower, the export was truncated |
email: N of M rows have a value | People with no address are not matched through their address — which is how Jira resolves, and how GitHub resolves unless you also declared a GitHub username column. If this number is far below your headcount, that is the ceiling on your Jira and GitHub attribution |
family_name_preferred: N of M rows have a value | Expected, and not a fault. A preferred surname is optional in most HR systems, and this is why the recipe reads both surname columns rather than one: the people with an empty cell here are named from family_name_legal. What would be a fault is family_name_legal being short too — those people cannot be named at all |
6. Turn it on
Still on the source's page:
- Press Approve.
- Press Enable.
The rows are not in Prism yet: the next ingest run reads the stored file. Until it has, this source answers nothing.
Make it the organizational directory
Enabling the source stores the roster. Telling Prism to resolve people through it is a separate, deliberate step:
- On the same page, press Designate as the organizational context stream.
Exactly one source can hold that role at a time, and it is the one every other source's per-person figures are resolved against. Prism will refuse to designate a second one while the first is enabled — which is deliberate, because "which directory does this installation believe" must never be decided by accident.
At the next ingest run Prism builds the crosswalk and per-person questions start working.
Check that it resolved anybody
Do this once, after the first run has been through. It takes a minute, and it is the difference between a directory that works and one that only looks like it does — the upload report in step 5 tells you what is in the file, and this tells you what the file could be joined to.
Open the self test and read the identity crosswalk line. On a healthy roster install it names your source and then counts what it linked, by kind:
identity crosswalk PASS csv_people is designated and its crosswalk compiled
from it: email:declared 1842, github_login:observed 1204
Only the links that resolve one person's identifiers to each other are counted. The manager column is a reporting edge rather than an identifier, so it never appears on this line — its absence says nothing about whether "everyone under X" works. Read what is there kind by kind, because a total would hide exactly the thing you came to check:
| The count | What it means |
|---|---|
email:declared | Address-to-person mappings, counted as they are written. Compare it with the email: N of M rows have a value figure from your upload; they should be close. A duplicate address does not show up as a shortfall here — both rows are counted and then flagged on the ambiguity line below, so read the two together |
github_login:declared | Your document declares a GitHub username column and it is working. This kind appears only if you added one |
github_login:observed absent | No evidence has been kept — which is not quite the same as nobody having asked. Either the sweep has not run (no ingest.github.orgs, switched off, no Members: Read, or simply not finished: it works through a large organisation over several ticks), or it ran and kept nothing, because your members publish no address, or because identityEmailDomains is set and none of them matched it. The run's own log distinguishes the two |
github_login:observed 0, the line present | Prism asked and got answers, and none of them matched an address in your roster. Not a broken sweep — a disagreement between what your people published on GitHub and what your export carries. Compare the domain first: a whole-estate mismatch (@acme.com against @corp.acme.com, or an acquired company's domain) produces exactly this, and a local-part mismatch rarely does |
github_login:observed well below email:declared | The expected shape rather than a fault: the gap is the people who published no address plus the people who published a different one. See How the address reaches GitHub |
… identifier(s) name more than one person | Two rows carry the same address or the same username, so it resolves to nobody rather than to one of them. Fix the export and upload again |
| linked no identifiers at all | The line WARNs. Either no run has read the roster yet, or email is empty in every row |
| the crosswalk was EMPTIED | The line FAILs, and nothing resolves at all — not GitHub, not Jira. A compile failed and Prism cleared the table rather than let a directory it could not rebuild go on answering. It names the reason; if you have just edited your document, the likeliest one is a column named in the dataset or a link that is not in columns: |
With both github_login counts absent or zero, no GitHub activity resolves to
anybody and no question crossing GitHub and Jira will answer — while every
Jira and every roster question goes on working normally. That asymmetry is what
makes it easy to miss.
7. Handing over to a real directory
This is the step the whole arrangement is designed around. When you connect a system of record — a warehouse view of your people, an HR feed — you move the role to it and the uploaded file stops being consulted.
The order matters, because the crosswalk is compiled from the new directory's rows, and it has none until its first run:
- Connect and enable the new source (Copy a Snowflake table or view once a day walks through one).
- Wait for its first run to land a copy, and check it did — for a snapshot, its first verification question counts the people its latest copy lists.
- On the organizational stream's pane, press Move to the new source. One act: the roster gives up the role and the new source takes it, so the installation is never without a directory in between. A new source that has not landed a copy yet is refused, with nothing changed.
- Wait for the next ingest run.
That run rebuilds the crosswalk from the new directory. The roster's own links are dropped — not merged, not preferred where the new directory is silent — because two directories disagreeing about who somebody is produces a confident wrong answer, which is the worst thing this product can do.
Neither source loses anything else in the swap: the file stays uploaded, the new source keeps its credential, and you can move the role back if the new connection turns out to be wrong.
Turning it off altogether
Pressing Withdraw on the roster's page, with nothing to move the role to, is enough to stop Prism resolving anybody through the roster. Per-person questions then report that identities cannot be resolved, rather than answering from a directory nobody designated.
To remove the file as well, Delete the source. That destroys the uploaded file with it.
Deleting is permanent. The roster is registered from a recipe, not seeded by the installation, so nothing writes the row back at the next restart — and Prism says so when you delete it. To have a roster again, register a new one from the recipe picker and upload the file afresh. (This changed: while the roster was a shipped source, a restart did restore an empty row.)
8. Keeping it current
Upload a newer export whenever you want; it replaces the previous one, and replaces that day's copy of the rows. Prism keeps ninety days of dated copies, so "who joined and who left between these two rosters" is a question the data can answer.
There is no schedule and there is nothing to configure. An uploaded file is read by the next ingest run after you upload it, and again whenever the document changes — because the same bytes under a different column list are different rows, and a corrected document that never reached the table would be a correction that did nothing. Re-uploading the identical file under the identical document is correctly not a reason to run.
Changing your columns later
Editing the document — adding a column your export turned out to have, deleting
one it does not, correcting a from: — is the ordinary correction and it keeps
your source: nothing is deleted and nothing has to be registered again.
Prism checks the edit against the file you already uploaded, while you are waiting for the answer. Reading your file is this source's connection test, and Prism holds the file — so when you save the document it reads that file again under your new column list before it writes anything.
- If it reads, nothing stops. The source keeps the state it was in. An enabled source stays enabled, keeps serving readers, and — if it is your organizational directory — keeps resolving people. This is the usual case, because the usual edit renames something in the model or adds a column your export already carries.
- If it does not read, the source goes back to
registeredand is switched off, and the page tells you which columns could not be found. That reading proved nothing about your new column list, so Prism throws it away rather than carrying it over. Upload a file the new document can read, then approve and enable as before. - If the source is your organizational directory and the edit would switch it
off, Prism refuses the save outright and changes nothing, because a
disabled directory takes every per-person answer on every source down with
it. Fix the document or upload a matching file first. (Through the API you
can override this with
"confirm_disable": truein the body; the page does not offer it.)
Either way, the upload is what checks your columns against your export, so a
from: you have just mistyped is caught by name — at the door, rather than six
hours later in an ingest run.
What Prism refuses, and why
| If the file… | Prism… |
|---|---|
| is missing a column your document declares | refuses the whole file, naming the columns it could not find. A column of blanks is a directory that looks loaded and matches nobody |
| has a header row repeating a name | refuses it. A CSV reader silently keeps the last one, and which of two email columns wins would depend on their order |
| has a row with the wrong number of cells | refuses it, by row number. Usually a comma inside a value that is not quoted, or a line break inside one |
| has no data rows | refuses it, and keeps the file you already had. A header-only export is what a filter that matched nothing looks like |
| has two rows with the same employee id, spelled the same way | refuses it, with how many rows repeat a key and how many distinct keys there are, and keeps the file you already had. Only one row per key can be stored, and the scheduled run refuses a roster it would have to shorten, so the upload now refuses it first (since 0.16.0; before that the upload succeeded with a note and the next run refused). De-duplicate the export and upload it again. Two ids that are spelled differently but become the same key under your document's type or transform — 007 and 7 in a number column, E1 and e1 in a lower-cased one — are not caught at upload yet: the upload succeeds and the next run refuses the file, with the counts, in the ingest block of the diagnostic bundle |
| is a spreadsheet workbook, or "Unicode Text" | refuses it by name and tells you which format to save instead |
None of these messages ever repeats a person's data. They name column names, row numbers and counts, so you can act on them and still paste them into a ticket.
Limits
- A file is at most 12 MB by default, and the ceiling is yours to move. A
roster of 55,000 people over eleven columns is about 7 MB, so the default is
sized for a staff list with room to spare. The limit is
app.rosterUploadMaxMiB, and it is tied to the app's memory limit: reading an upload costs about ten times its size in memory while the file is parsed, so raiseapp.resources.limits.memoryfirst and this second, together, and expect a 32 MB ceiling to need more than the default 512Mi. The refusal names the value. A full HR export that exceeds the default has usually outgrown a hand-cut file and wants a connected directory — Handing over to a real directory. - One file per installation, not one per team.
- The columns are yours to declare, and a declared column is a required one: Prism refuses a file that is missing one rather than loading it half-empty.
- The file is read as CSV only. No workbooks, no other formats.
- Uploading is an administrator action; there is no scheduled fetch, by design.
- A person with no email address in the file resolves to nobody through their
address, however complete the rest of their row is — a declared
github_loginstill resolves their GitHub work, because that link is keyed on the employee id rather than on the address. - Resolved through the address alone, GitHub is less complete than Jira. Jira discloses an address on the record itself; GitHub discloses none, so the address route runs through what each member chose to publish on their profile, and a member who published nothing — or published something your export does not carry — is not resolved by it. Declaring the username is what closes that gap, and is the only thing that does.