Take a source live
Each source — GitHub, Jira, Router spend — flips from synthetic to live independently, as its access lands. This is that flip, end to end.
It is two helm upgrades, not one, and the order is the whole point of the
procedure. The first live ingest for a source truncates that source's
synthetic rows before writing real ones, so a credential that authenticates
cleanly but sees nothing leaves you with an empty source where you previously
had a working demonstration — and from the chat, an empty source is
indistinguishable from a broken product. The first upgrade proves the
credential; the second one flips.
The worked example below is GitHub. Jira and Router spend differ only in which values they set, and both are called out at each step.
1. Put the credential where the chart will read it
| Source | Chart value | Secret key |
|---|---|---|
| GitHub | secrets.githubToken | github-token |
| Jira | secrets.jiraToken | jira-token |
| Router spend | secrets.valetApiKey | valet-api-key |
With a chart-managed Secret that is a --set on the upgrade below. With
existingSecret the chart renders no Secret and cannot add a key to yours, so
patch it yourself first:
kubectl patch secret <your secret> -n <namespace> \
-p '{"stringData":{"github-token":"<the token>"}}'
Do this before step 2 on an existingSecret install. The probe reads that
key, and a key that is absent when the pod is created is reported as
no_token rather than found later.
2. Name the target, and do not flip yet
helm upgrade <release> <chart> -n <namespace> -f <your values file> \
--set ingest.github.orgs=<orgs>
ingest.github.orgs is a comma list, and one credential is read for all of it —
which is a constraint on the token rather than on the chart. A fine-grained
personal access token can only reach resources owned by a single organisation,
so an install naming two of them needs a classic PAT that is a member of both,
or one Prism per organisation.
What the GitHub credential needs is the
rest of that list, and is worth reading before the token is minted rather than
after the pre-flight says it saw nothing.
Jira sets ingest.jira.url instead. Router spend names its target in
mcps.management.valetServiceUrl — the Agent Router management plane, a
different address from the inference gateway in router.proxyUrl, and the one
your admin key belongs to. Set it in step 1 alongside secrets.valetApiKey:
until 0.8.0 it carried a default that was Tetrate's own Router, which meant a
flip appeared to work and mirrored nothing. It ships empty now, and the flip
below refuses to render without it.
Note what is not in that command: sources.github=live. The source keeps
serving synthetic data through this upgrade.
Decide the request rate here too, if your instance is one you would rather be
careful with. Prism calls each source at a ceiling you set, in requests per
minute — ingest.github.maxRequestsPerMinute and
ingest.jira.maxRequestsPerMinute, both 30 by default, one request every two
seconds. That default is chosen for an upstream nobody has measured yet, which
is the normal case for a self-hosted GitHub Enterprise Server or Jira Data
Center. Lower it if your platform team wants to start smaller:
--set ingest.github.maxRequestsPerMinute=10
The cap covers retries as well as first attempts, so an ingest being
rate-limited by your instance cannot answer by calling it faster, and the only
cost of a low number is a longer run — nothing is skipped. Raise it later with an
ordinary helm upgrade; nothing needs re-ingesting. The rate in force is the
first line of every ingest log (step 5), and
limits.md has the detail.
The rate Prism is actually calling at — as opposed to the ceiling you just set — is on What Prism is pulling, right now, live, with no shell needed. Open it before step 5 and leave it open; it is the page to have up while the first backfill runs, and the page to send anyone who asks what is calling their instance.
3. Read the verdict
That upgrade runs prism-ingest check github as a hook, against the credential
and target the ingest itself would use, and the verdict lands on the
self test beside a source still serving synthetic data:
PASS github synthetic · 26,029 rows · seeded 6d ago · upstream ready to go live (1m ago)
WARN github synthetic · 26,029 rows · seeded 6d ago · upstream NOT ready:
org_visible_but_no_prs_found (1m ago) — going live now would
evict the rows above and ingest nothing
Read the amber line as "do not flip yet". Nothing is broken — the source is serving what it is meant to serve on day one — and the flip is the step that would turn a wrong credential into an empty source. Fix the credential, upgrade again to re-run the probe, and only go on when the line reads green.
On an existingSecret install there is no pre-flight for Router spend: the
chart reads that signal from secrets.valetApiKey and cannot see inside a
Secret it does not own. Nothing is at risk in flipping spend without it — the
spend index is its own table, so a sweep that ingests nothing evicts nothing.
GitHub and Jira are unaffected, because their targets are values.
4. Flip
helm upgrade <release> <chart> -n <namespace> -f <your values file> \
--set ingest.github.orgs=<orgs> --set sources.github=live
The other two:
--set ingest.jira.url=<your Jira> --set sources.jira=live
--set mcps.management.valetServiceUrl=<your Router management URL> \
--set sources.spend=indexed
There is no sources.directory any more. Prism kept its own table of
people — who exists, which GitHub login belongs to which Jira assignee, which
team somebody is on — and it is retired. What replaces it is a data source you
connect like any other and then designate as your organizational context
stream, on Admin → Context streams. See
One stream must carry your org directory
for what that changes in the answers; the short version is that until you
designate one, Prism will say a person cannot be resolved rather than give you
a figure it cannot stand behind.
sources.spend takes indexed or live, and indexed is almost always the
one to want; step 7 of the install has the reasoning.
Spell the mode exactly, in lower case. Anything else — Live, LIVE, a
typo — fails the helm upgrade immediately, naming the key and what you typed.
That is deliberate: an unrecognised mode used to install cleanly and start no
ingest at all, while the app served the mirror the ingest was meant to fill, so
the source went empty and stayed empty.
There is a second, unrelated flip, and it is worth not confusing the two. This one takes a source from synthetic data to your real system. The other moves a live source from its legacy lane onto the manifest ingest engine, and is Flip a source to the ingest engine.
5. Watch the first backfill, and do not read the prompt as "done"
helm upgrade returns as soon as the backfill has started. The first run
walks ingest.backfillDays (90 by default) of history for every org or project
you named — over an hour, measured at 77 minutes for one GitHub org
(8,965 pull requests and 14,386 issues, walked together) — so expect it to still
be running long after the command comes back clean. A larger org takes longer;
a backfill still going after an hour is normal, not stuck.
In ingest.engine.mode: authoritative, add roughly 9–18 minutes per GitHub
organisation at the default 90 days: issues reach back at least 365 days
there whatever ingest.backfillDays says, and each day past the pull-request
window costs one issue-search request per 100 issues updated that day — one or
two at the density the figure above was measured at — and nothing else. At
ingest.backfillDays of 365 or more there is nothing to add — the floor is
not a cap, so both lanes walk the same window and the figure above already
covers it.
kubectl get jobs -n <namespace> -l app.kubernetes.io/component=github-ingest
kubectl logs -n <namespace> -l app.kubernetes.io/component=github-ingest -f
It prints a line per day bucket, so progress is visible rather than inferred.
What Prism is pulling, right now shows the same progress
without a shell — how far through the window the backfill is, the rate it is
running at, and how your instance is answering — and it is what to watch if the
people on the call do not have kubectl. Admin → Context streams shows the row
count filling in, and the self test reports the source as populated once it
has. A source still showing zero rows after that Job has
finished is a credential or egress problem, not a slow backfill — which is the
state step 3 exists to have caught already.
Two things that look wrong and are not: re-running an identical flip command does nothing (the Job is named after its own configuration, so Helm no-ops rather than restarting a running backfill), and a second Job completing in seconds is the CronJob firing mid-backfill, finding the source already claimed, and exiting successfully rather than racing it.
6. While the backfill runs: what the source can and cannot answer
The flip empties the source's table before it refills it — the reason this page is two upgrades rather than one — and the refill is oldest day first. So between the flip and the moment that Job finishes, the source holds the start of your history and none of the recent end. On a low request rate that window is hours; on a very low one it is days.
That is a supported state, and Prism labels it. While the first backfill is unfinished, every answer from this source reports the day it has actually reached rather than the moment its rows were written, so a question about last month comes back marked as running past coverage — a floor, with the uncovered part named — instead of a confident figure over a table that does not hold the data yet. Nothing needs turning off, and the mirror is never a mix of synthetic and real rows.
Two levers if the wait is too long, and they are the only two worth pulling:
-
Raise
ingest.github.maxRequestsPerMinute, once your platform team can tell you what the instance will actually take. Backfill time is very nearly inversely proportional to it. -
Lower
ingest.backfillDaysfor the first pass — 30 days of history arrives in a third of the time — and widen the window later withingest.<source>.rewalkFrom, below. Raisingingest.backfillDaysafter this first backfill has finished does nothing: it is read only where a source has no history at all.In
ingest.engine.mode: authoritativelowering this lever moves the pull request walk only. On a source the engine has not ingested before, GitHub issues reach back at least 365 days however low you set it, so a 7-day first pass still walks a year of issues — roughly 12-24 minutes per organisation at the default request cap, the top of that range at the density the figures above were measured at. On a source the legacy lane has already walked, the engine reads that lane's recorded issue horizon out of the sharedingest_staterow and the floor does not apply;ingest.<source>.rewalkFromis the lever that recovers the year. Raising it past 365 is the other story: the floor is not a cap, so both lanes then walk the whole window and the issue lane costs a full bucket per day, not one request. See Issues reach back a year.
Do not shorten ingest.runDeadlineSeconds to make an interrupted backfill
resume sooner. It is the one change that can stop a slow backfill converging
altogether: progress is recorded per completed day bucket, so a run killed
before it finishes one records nothing, and the run after it reads that as a
first run — truncating and starting over, every firing. (The ingest warns in its log
where a busy bucket will not fit, and refuses to start where not even an empty
one would, rather than looping silently — but stay well clear of the edge. The
Jira lane records its progress per page of search results and refuses a deadline
too short to record one, in exactly the same way.) If the aim is to resume sooner after a kill, tighten
ingest.github.schedule instead — that shortens the idle gap without shortening
the window a bucket has to finish in.
Do not add an org to ingest.github.orgs while the first backfill is in
flight. An interrupted run records which orgs it covered, and a resume point
cannot vouch for ground it never walked — so a run that wants a wider list
correctly refuses to resume, and starts the whole backfill again from empty.
Wait for the Job to finish, then add the org.
What the GitHub credential needs
This is the list to hand your GitHub admin before the token is minted. Every item on it is a way for a credential to authenticate cleanly and then either empty the source or quietly undercount it, which is the failure this whole procedure exists to catch — arrived at from the one direction the checks above do not cover.
Prism does not care which kind of token it is. The ingest sends
Authorization: Bearer <the token>, which classic and fine-grained personal
access tokens both accept. What differs is what you have to ask for.
A GitHub App is the third kind, and it is configured differently — three values rather than a token, because Prism mints the token itself. The permission list below is the one to ask for either way; Widen a source's scope has the procedure. Prefer it wherever the choice is open: an installation token's scope is the App's grant rather than a person's visibility, it carries no user seat, and it gets higher rate limits. A fine-grained token scoped to All repositories is a perfectly good credential for an organisation of any size, and is the simpler place to start.
Classic PAT: repo + read:org, and SSO-authorised for the org wherever
SAML is enforced. repo is the part to be uneasy about — GitHub has no
read-only private-repo scope, so it grants write as well. Prism never uses it;
if your organisation is minting for least privilege, a fine-grained token is
the answer, and the four notes below are the price of it.
Fine-grained PAT:
| On the token | What to ask for |
|---|---|
| Resource owner | the organisation you are measuring — one per token |
| Repository access | All repositories |
| Repository permissions | Pull requests: Read · Issues: Read · Metadata: Read |
Repository permissions, if mcps.githubLive.enabled | the three above plus Contents: Read · Checks: Read · Commit statuses: Read |
The first row is the whole list for the ingest, which is what this procedure is about: no Contents, nothing at organisation level, and no write of any kind. What the ingest stores is pull-request and issue metadata — authors, timestamps, states, labels, review outcomes, and the added/deleted line counts — never a diff, and never the body of a comment or a review.
The optional GitHub Live MCP reads the same github-token, so it widens that
one list rather than getting a list of its own. It exists to open one named
pull request on request, and its detail tool can return that PR's diff, its
changed files, its comments and its CI checks — so it needs more than the three
permissions above, and it changes the answer to "does Prism read our code". It
is off by default (mcps.githubLive.enabled) and is not in every deployment.
If you are turning it on, add three grants to the token you are already
minting — Contents: Read (the diff and changed-files views), Checks:
Read and Commit statuses: Read (the CI-checks view; these are two
separate GitHub permissions and the CI aspects fail with Resource not accessible by personal access token without them). Still nothing at
organisation level, and still no write.
Why one list and not two. Every deployment Prism ships wires one credential to both halves: the chart's ingest Jobs and the live MCP both read secret key
github-token, compose gives both${GITHUB_TOKEN}, and the Cloud Run scripts mount one secret into all three consumers. So a grant list written for the live MCP alone would drop Issues: Read, and the ingest's issue half would authenticate cleanly and ingest nothing — the exact silent failure this procedure exists to catch. Giving the live MCP a credential of its own is real work on the chart, not a wording change; it is tracked (prism #572) and a release note will say when it lands. Until then, the union above is the honest ask.
Issue this one fine-grained even if you issue nothing else fine-grained. A
classic token cannot read a private repository without the repo scope, and
repo also grants write — push, merge, branch deletion — so the live MCP
would be holding a credential that can change your source code in order to read
a pull request, with a single setting inside the image (GITHUB_READ_ONLY=1,
baked in) as the only thing between the two. A fine-grained token has no write
to withhold.
1. One resource owner per token. A fine-grained token reaches resources
owned by a single user or organisation — that is the design, not a setting.
ingest.github.orgs is a comma list and the ingest reads one credential for all
of it, so an install measuring two organisations cannot be done with one
fine-grained token. A classic PAT on an account that can see both, or one Prism
per organisation, are the two ways out. Decide it before minting rather than
relying on the pre-flight: what step 3 reports for the second organisation
depends on how visible it is to a token that does not own it — org_not_visible
where it is private, org_visible_but_no_prs_found where the organisation page
is readable but its pull requests are not, and nothing at all where it has
public repositories, because their pull requests are found and the check goes
green on a fraction of the org.
2. "All repositories", not "Only select repositories". This is the nastier
one, because it does not fail. A token scoped to a subset of repositories walks
successfully and ingests that subset; the pre-flight passes, the self test is
green, and every org-wide figure is a subtotal wearing the label
org-wide (every repo in the index). Nothing anywhere names the repositories
that were never fetched. If your organisation insists on an allowlist it has to
list every repository you intend the figures to cover — and somebody has to
remember it when a new one is created.
3. Three repository permissions — six with the live MCP — and nothing at
organisation level. Pull requests: Read is the core grant — the PR list, the sizes, and the review
timestamps behind time-to-first-review. Issues: Read is the second and it is not
optional: Prism ingests issues as well as pull requests, and search returns only
what the token can see. GitHub does not document which grant surfaces a private
issue in search, so treat this as the one to prove rather than assume — run
the third curl below a second time with type:issue in place of type:pr, and
expect a count from both. Metadata: Read is GitHub's mandatory baseline,
selected for you as soon as you pick any other repository permission. No
organisation permission is needed, which is worth saying because it is the
natural thing to reach for: the one organisation-scoped call the ingest and the
pre-flight make is GET /orgs/<org>, and GitHub documents that endpoint as
requiring no fine-grained permission at all. A 404 there is a question about
approval or visibility — note 4 — and no organisation grant will fix it.
4. Organisation policy, and an owner's approval. Two switches sit above the token and neither is yours: the organisation has to permit fine-grained tokens at all (it can block them, and on GitHub Enterprise Server an enterprise-level policy sits above that), and where the organisation requires approval an owner has to approve this specific token. An unapproved token is not a rejected one — it authenticates, and keeps the read-only access to public resources that every fine-grained token carries. What that looks like depends on your organisation, and neither shape is good:
- All repositories private, which is most Enterprise Server installs. The
token sees nothing, and the pre-flight says
org_not_visibleororg_visible_but_no_prs_found— exactly what a firewall or a repo-scope problem says. Check the organisation's personal access token settings before you go to the network team; it is much the cheaper of the two to check. - Some repositories public. Worse, because it goes green: the token can see those, the search finds their PRs, the pre-flight passes, and you flip a source that will mirror the public half of your organisation and label it org-wide. That is note 2 again, reached by a different route.
GraphQL is a second hop, and the pre-flight does not gate on it. The ingest
makes a GraphQL call per day bucket for PR sizes, reviews and issue-to-PR
links — a different endpoint from the REST search, which is why verifying by
hand asserts it separately in step 4. Step 3 of this procedure records that hop
and then decides without it, so a credential that passes the REST assertions and
cannot reach /graphql is still reported ready to flip. Run step 4 below with
the token you are about to install, not with your own. Token type is not the
usual reason it fails — fine-grained tokens could not call GraphQL when they
launched, but GitHub added that in April 2023 and Enterprise Server did not
offer fine-grained tokens until after it, so on any instance that can mint one
the answer is an instance or proxy blocking /graphql, not the token. A source
flipped on without that hop still ingests — the run leaves those columns null on
the days it could not enrich, writes down which days those were, and finishes.
Step 4 below says how to read that back.
Expiry. A fine-grained token's lifetime is chosen when it is minted. GitHub allows a non-expiring one, but your organisation or enterprise can forbid that and can cap the maximum well under a year — so find out which you have been given rather than assuming. If it expires, record the date and name the person who owns the rotation at go-live, while somebody is thinking about it: the scheduled probe does report an expired credential, but it reports it on the day it stops working, and a token nobody owns is a token nobody renews. Rotate a credential is that job.
Verifying by hand, where there is no cluster yet
Before the first install there is nothing to run a hook, and the same three assertions are worth making from a pod in the target namespace. These are the ones the probe makes, in the same order:
GitHub. From a pod in the namespace, with the token you are about to install:
# 1. Does the credential authenticate, and what is it actually granted?
curl -sS -D- -o /dev/null -H "Authorization: Bearer <token>" \
"<your GitHub API URL>/rate_limit" # 200, and read the x-oauth-scopes header
# 2. Is the org visible to it?
curl -sS -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer <token>" \
"<your GitHub API URL>/orgs/<org>" # 200
# 3. The assertion that matters: are that org's PRs findable?
curl -sS -H "Authorization: Bearer <token>" \
"<your GitHub API URL>/search/issues?q=org:<org>+type:pr&per_page=1" # total_count > 0
# 4. GraphQL is a separate hop, and GHE can serve REST while blocking it
curl -sS -o /dev/null -w '%{http_code}\n' -X POST -H "Authorization: Bearer <token>" \
-d '{"query":"{ __typename }"}' "<your GitHub GraphQL URL>" # 200
Step 3 is the one to care about. A token can authenticate, see the org, and
still return total_count: 0 — and that is the case that empties your source.
Do not settle for step 1 because the scopes look right, and note that a
fine-grained token returns no x-oauth-scopes header at all: absent grants and
an absent header are the same reading, which is another reason the assertion
that counts is the search. What that token should have been granted, either
kind, is above.
Step 4 failing is narrower but not harmless: PR counts keep working and every
PR size lands null, along with reviews and issue-to-PR links. Step 3 does not
gate on this hop, so a credential that passes the pre-flight can still fail here
— which is why it is worth running with the token you are about to install
rather than with your own.
The signal that says it is happening. The ingest does not fail on a dead
/graphql: it abandons that day's enrichment, carries on, and finishes. What it
leaves behind is the record — the run log carries a WARNING naming the buckets,
and the durable list is unenriched_days in the github row of ingest_state:
psql "<your datapond URL>" -c \
"SELECT detail->'unenriched_days' FROM ingest_state WHERE source = 'github'"
Three readings, and they are different:
| value | meaning |
|---|---|
| absent | this mirror was built before the record existed, or by a source that keeps none |
[] | the ingest ran and enriched every day it walked |
| entries | those org/day buckets have their pull requests and nothing else |
An entry names its reason, and clears itself: fix the hop, re-run the ingest
over those days, and the bucket that comes back complete drops out of the list.
Nothing else removes an entry, so a list that stays full after a re-run means
the re-run did not reach /graphql either.
On GitHub Enterprise Server the API base is https://<host>/api/v3 and GraphQL
is at https://<host>/api/graphql; on github.com they are https://api.github.com
and https://api.github.com/graphql.
Jira. Same shape:
# 1. Reachable, and which deployment — Cloud and DC page differently
curl -sS "<your Jira base URL>/rest/api/2/serverInfo" # 200, note deploymentType
# 2. Does the credential resolve to a user? serverInfo is anonymous on many
# DC installs, so on its own it proves nothing about auth.
curl -sS -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer <PAT>" \
"<your Jira base URL>/rest/api/2/myself" # 200
# 3. Are issues actually visible to it? (Data Center form)
# THIS IS THE UNSCOPED FORM, and it is what the probe asks only when
# ingest.jira.projects is EMPTY. If you have set it, run 3b instead — a
# healthy `total` here proves nothing about the query the probe will send.
curl -sS -X POST -H "Authorization: Bearer <PAT>" -H 'Content-Type: application/json' \
-d '{"jql":"order by created DESC","startAt":0,"maxResults":0}' \
"<your Jira base URL>/rest/api/2/search" # total > 0
# 3b. If you set ingest.jira.projects, THIS is what the probe asks. Jira refuses
# the WHOLE query with a 400 if a single key is unknown or invisible to the
# credential — it does not answer for the keys that were fine.
curl -sS -X POST -H "Authorization: Bearer <PAT>" -H 'Content-Type: application/json' \
-d '{"jql":"project in (\"ABC\",\"DEF\")","startAt":0,"maxResults":0}' \
"<your Jira base URL>/rest/api/2/search" # 200, total > 0
# 3c. Where those keys come from — the ones this credential can actually see,
# rather than the ones a spreadsheet says it should.
curl -sS -H "Authorization: Bearer <PAT>" \
"<your Jira base URL>/rest/api/2/project" # .[].key
Jira Cloud authenticates with ingest.jira.email plus the API token as HTTP
basic auth rather than a bearer PAT (curl -u), and counts through
/rest/api/3/search/approximate-count. Data Center uses the bearer PAT above.
While you have the credential to hand: if this install has ever run a live
Jira source on a release before 0.9.0, or runs now with
ingest.jira.absoluteWindowBound turned on, also run
Am I affected? — the same two shapes of call,
answering whether that credential's timezone skewed the incremental window.
If 3b comes back 400 and you do not know which key it is, you do not have to
bisect it by hand: the probe does that for you. On a refusal it re-asks one key
at a time and records the verdict for each — see the fault list at the end of
this section, and project_verdicts in the diagnostic bundle.
Widening the window later
Start small if you want to — 7 days first on a rate-limited instance is a good
first move. What does not widen it afterwards is raising
ingest.backfillDays:
ingest.backfillDaysis read only on a source's first run. Once that backfill finishes the source has a watermark, and every run after it walks forward from there. Raise the value,helm upgrade, and you get a clean upgrade, a Job that runs, and the same window you had before.
Nothing goes wrong when that happens, which is what makes it worth stating: the
mirror reports its own horizon honestly, so answers stay correctly scoped to the
history you have. They are just scoped to 7 days while you believe they are
scoped to 90. (An ingest run that notices the gap now says so in its log —
INGEST_BACKFILL_DAYS=90 … IT IS NOT CONSULTED ON THIS BRANCH.)
The supported way to widen it is a re-sweep: name the date you want the source to reach back to.
helm upgrade <release> <chart> -n <namespace> -f <your values file> \
--set ingest.github.rewalkFrom=2026-03-01
and the same for Jira, separately — the two are deliberately not one value, so widening one source during a staged go-live does not restart the other:
--set ingest.jira.rewalkFrom=2026-03-01
What it does, and does not do:
- it walks the ground between that date and where the source is now, and upserts what it finds. Nothing is truncated — this is a repair, not a first run, so the source keeps answering throughout;
- it starts as soon as the upgrade lands (the ingest Job's name is a hash of its own configuration, so a changed date is a new Job), and it costs what a backfill of that length costs. Watch it the way you watched the first one;
- it happens once per date. Leave the value in your values file: the run that carries it out records the date, so the 6-hourly schedule does not re-walk those months on every firing. To re-sweep again — a wider window, or another attempt at one that was killed — name a different date;
- coverage already earned is never given up. Re-sweeping the last six weeks does not make the source claim six weeks of history;
- under
ingest.engine.mode, both lanes re-walk. The engine keeps its own copy of the source's bookkeeping, and a repair that reached only one of the two could never close a gap between them. Each lane records the date separately, so neither re-walks on the other's claim and the "once per date" rule above holds for both. Budget two passes rather than one, and the engine's is the slower: it runs at half the source's rate unlessingest.engine.shadowRequestsPerMinutesays otherwise; - blank the value before you first arm the engine. A date left in your values
file after a repair is inert for the lane that carried it out, but the engine's
lane has never seen it — so the upgrade that first sets
ingest.engine.modetoshadowstarts a full re-sweep from that date at the next tick, unasked. This is the one case where leaving it set is not free.
Check it landed with the horizon the source reports on Admin → Context streams, or:
kubectl exec -n <namespace> statefulset/<release>-postgres -- \
psql -U postgres -d prism -c "
SELECT source, detail->>'backfill_horizon' AS horizon,
detail->>'issue_backfill_horizon' AS issue_horizon, detail->'rewalk'
FROM ingest_state;"
GitHub reports two horizons, because it walks two searches: horizon is how
far back its pull requests reach and issue_horizon how far back its issues do.
Each answer is scoped to the one it came from, so a pull-request question and an
issue question over the same source can honestly be labelled with different
coverage.
A re-sweep moves issue_horizon only in ingest.engine.mode: authoritative.
In the default off mode the issue search does walk the re-swept ground and the
rows land, but the recorded issue horizon stays where it was — so issue answers
over the recovered months keep saying they are past coverage until the source is
ingested by the engine. Pull-request coverage moves either way. (Under the
engine the issue horizon also starts a year back on a new install; see
Issues reach back a year.)
The horizon moves back only when a re-sweep finishes. A re-sweep that was
killed part way (ingest.runDeadlineSeconds, an evicted pod) keeps every row it
already wrote but does not resume by itself and does not move the horizon — so
if the horizon has not moved, name a nearby date and run it again.
When a shadow parity verdict will not clear
Only in ingest.engine.mode: shadow. The engine walks each source beside the
lane that already serves it, compares the two, and the self test counts
consecutive passing comparisons towards ingest.engine.parityRuns — the
evidence that the engine can be trusted with the lane's job. A diff resets
that count to 0.
Almost always a diff means what it says, and the verdict names the tables and
rows. There is one cause that does not clear by itself, and it is worth
knowing because it looks identical from the outside — diff after diff,
0/3 that never climbs, and nothing wrong with the engine:
Your upstream had a bad afternoon while the engine was backfilling. The engine recorded the day it could not fetch in full, and carried on — which is the right behaviour, and the record is a true fact about its mirror. But the lane it is compared against walked that same day months ago, when the upstream was healthy, so only the engine carries the record. That is a real difference and it is reported as one. It is also permanent: an ordinary run starts at the watermark and never walks a settled day twice, so nothing reaches back to repair it, and it stays inside the compared window for as long as
ingest.backfillDaysreaches that date.
The verdict says so and names the dates and the value that clears them — but
not on a page you can read at a glance. /selftest shows the status and the
count only (shadow parity: github diff 0/3); Admin → Context streams does not
carry this verdict at all, the parity badge there being a different check.
There are two ways to the rest, and they do not give you the same thing:
- press Download on
/selftestand readshadow_parity.<source>.summary. That sentence names the key, the earliest date and the value to set, which is enough to act on — but the self test keeps only a summary of the verdict, so the full date list is not in that file; - or read the verdict itself out of the database, which has everything:
kubectl exec -n <namespace> statefulset/<release>-postgres -- \
psql -U postgres -d prism -c "
SELECT jsonb_pretty(detail->'parity'->'state')
FROM ingest_state WHERE right(source, 8) = '__shadow';"
An entry under capped_days or unenriched_days carrying shadow_only_days
(or legacy_only_days) is this case. The shadow_remedy / legacy_remedy
beside it is a re-sweep from the earliest date that entry lists, exactly as
in the section above:
helm upgrade <release> <chart> -n <namespace> -f <your values file> \
--set ingest.github.rewalkFrom=<the earliest date the verdict lists>
One cycle later the engine has walked those days against a healthy upstream, the record is cleared by the walk that cleared the cause, and the next comparison is made without it. The count starts from the run after that.
The same thing happens in mirror image, and it is the same repair. An entry
carrying legacy_only_days is a day the lane could not fetch, on ground the
engine walked cleanly. It is just as sticky, for the same reason — the lane's
incremental runs never revisit a settled day either — and the re-sweep clears it
from the same one line, because the value re-sweeps both lanes. The verdict says
which lane in every case; read shadow_only_days / legacy_only_days rather
than assuming the engine is the one at fault.
Two things to expect:
unenriched_daysalways clears;capped_daysmay not. An unenriched day has all its rows and is missing columns, so re-walking it against a working upstream completes it. A capped day is one whose rows were past a ceiling the upstream imposes — the record clears only where the re-walk gets that day whole, and where it cannot, the engine genuinely holds less than the lane does. That is a difference worth keeping;- the count restarts rather than resumes, and it restarts at the first
comparison over a window that has moved. Give it
ingest.engine.parityRunscycles before reading anything into the number.
Nothing here is urgent. shadow is a measurement mode: the lane that serves
your users is the one that always served them, and it is untouched throughout.
Once the count does read ready, Flip a source to the ingest
engine is what to do with it — including how to move one source
without moving the others, and how to put it back.
Afterwards, the schedule takes over
The CronJob re-probes on ingest.checkSchedule, and you can run it on demand
rather than waiting:
kubectl create job -n <namespace> probe-github --from=cronjob/<release>-github-check
The probe exits 0 whatever it finds — a failed Job here is Prism's database,
not your upstream. An upstream fault comes back as a red
upstream check failed: <fault> line on the self test instead.
What a pass does not prove. That the credential still works tomorrow. A token revoked after go-live leaves a row count identical to a healthy one, which is exactly why the probe keeps running on a schedule and why the self test distinguishes "rows are here" from "the upstream is still answering".
If it still fails: the probe's evidence in the diagnostic bundle names the
fault — bad_credential, org_not_visible, org_visible_but_no_prs_found,
unreachable, timeout. Send that; it is the difference between a scope
problem and a routing problem, and it carries no repository names, issue keys
or logins.
The two Jira lanes add two more, and the difference between them decides who picks the ticket up:
| fault | what it means | who fixes it |
|---|---|---|
projects_not_visible | one or more of the keys in ingest.jira.projects (or JSM_PROJECTS) is unknown to this instance or invisible to this credential. project_verdicts.refused names them, and the self-test line carries them too | you, in values.yaml — or your Jira admin, if the key is right and the grant is not |
no_issues_visible | every configured key resolved, and the credential can see no issues in any of them | your Jira admin: this is a permission or a genuinely empty scope, not a typo |
edition_mismatch | ingest.jira.deployment says one edition and the instance says it is the other — cloud declared against a Data Center, or the reverse. The engine-managed Jira stream runs the declared edition and refuses on this before it walks | you, in values.yaml: set ingest.jira.deployment to the edition the evidence names |
project_verdicts also carries readable_but_empty — keys that resolve and
hold nothing. Those are not wrong keys, and correcting them is how a working
configuration gets broken.
A source Prism queries in place, rather than ingests
Everything above copies a source's data into Prism's own database and answers out of that. A query-through source does not: nothing is copied, and every question about it becomes one statement against your warehouse, run as a login you control. Today that means Snowflake, and it needs the semantic query MCP, which ships on since 0.10.0.
Most installs set none of this and that is the normal case, not a gap. Skip this section unless you have been asked to connect a warehouse.
What the chart needs for a source you connect in the admin surface
No switches and no credentials — both of these are the default since 0.10.0. Set them only to be explicit, or if your values file pins either to false:
mcps:
semanticMcp:
enabled: true # the only component that executes a query-through statement.
# DEFAULT since 0.10.0
sources:
registry:
enabled: true # the source registry: a query-through source is a row in it.
# DEFAULT since 0.10.0
sources.registry.enabled is what renders ENCRYPTION_KEY into both the app
and the semantic MCP, from the Secret entry your install already has. The app
encrypts a source's credential with it; the semantic MCP opens it to run that
source's statements. Without the registry there is no row, so there is nothing
to connect and the semantic MCP will say so.
mcps.semanticMcp.enabled is not only for Snowflake. It is what puts the
semantic query tools on the agent, and those are the only tools that reach a
registry source's statement — so the source you designate as your
organizational directory is unreadable to the agent without it, whatever kind
it is. An uploaded staff list needs it as much as a warehouse view does: the
self test's designated route line fails without it, and says so by name.
This section is a prerequisite for Connect your Snowflake
directory and Upload a staff
list, not an upgrade note — the subsection below is the
upgrade note.
None of it is a chart value. Earlier releases put the account, the login and
the private key in values.yaml and in a Secret, and that was wrong in a way
worth stating, because you may have read the older page: it made two
connections for one warehouse. The admin page's probe authenticated with the
credential stored on the source, the semantic MCP executed with the values in
the chart, and nothing compared them. A source could show as connected and
answer nothing.
There is one connection now and it belongs to the source. You enter it once, in the admin surface, and the probe that switches the source on is a probe of the connection that will answer.
Upgrading from a release that put this in values.yaml
If your values file still carries mcps.semanticMcp.snowflake.* or
secrets.snowflakePrivateKey, the upgrade will refuse to render and name
this page. That is deliberate: the chart no longer reads those keys, so
rendering on would drop the warehouse credential you supplied without a word and
leave a source that shows as connected and answers nothing.
The migration:
- Set
mcps.semanticMcp.enabled: true— and delete themcps.semanticMcp.snowflakeblock and bothsecrets.snowflakePrivateKey*keys.sources.registry.enableddefaults totruefrom 0.10.0, so set it only if your values file pins it tofalse. - Upgrade.
- Enter the connection and the private key on the source (below), and probe it.
Your Snowflake key pair does not change and neither does the grant. What changes is where the appliance reads them from — which is what stops a source probing against one warehouse and answering from another.
What to enter, on the source
Sign in as a super admin, open Admin → Context streams, and give the source its connection parameters and its credential.
| Parameter | What it is |
|---|---|
account | your account identifier — myorg-myaccount, or a legacy locator. The host Prism posts to is <account>.snowflakecomputing.com |
user | the service login Prism signs in as: a TYPE = SERVICE user with your read-only role granted. It is configuration, not a secret — it appears in QUERY_HISTORY and in every authentication failure, and somebody has to be able to read it to fix one |
warehouse | the virtual warehouse the statements run on. This is what your spend lands on, so give a query-through source its own if you want its cost separated |
database | the database holding the view |
schema | the schema holding the view. Optional: leave it empty if the login's default schema is the right one |
The credential is the private key of the pair whose public half is registered
against that login — a PKCS#8 PEM, the -----BEGIN PRIVATE KEY----- form.
Paste it into the credential field on the source, with its passphrase if it has
one. Snowflake is retiring password sign-in for TYPE = SERVICE users through
the August–October 2026 window, so a key pair is the credential that will still
work.
PEM, and nothing else. A .p8 extension says nothing about the encoding:
openssl pkcs8 -topk8 -nocrypt writes PEM, and -outform DER writes binary
under the same name. Check before you paste — a PEM file's first line is
-----BEGIN PRIVATE KEY-----:
head -1 rsa_key.p8
If that shows binary rather than a BEGIN line, convert it:
openssl pkcs8 -topk8 -nocrypt -inform DER -in rsa_key.p8 -out rsa_key.pem
Prism checks the key when you enter it rather than at the first question, so a key it cannot read is a refusal on the credential form and not a source that looks connected and answers nothing.
The role is not a parameter, and that is deliberate. PRISM_READER — the
read-only role our recipe tells you to grant — is written into the source's
manifest as a literal, and every session activates it. So the ceiling on what a
question can read is the grant you made, not a value somebody could forget to
set. Grant it USAGE on the warehouse, the database and the schema, and
SELECT on the one view; nothing else. The schema grant is the one most often
left out — it is the only one that is not about an object you named — and
without it the view is invisible to the role however correct everything else is.
Proving it, which is not the same as entering it
- Probe the source from the admin page and read the verdict. It reports the session's user, role, warehouse, database and schema as Snowflake established them — and it compares every one of them with what it asked for. This is the case worth knowing about: Snowflake does not refuse a warehouse or a schema that does not exist. It accepts the request and quietly gives you a session without one, and every real question then fails. So the probe reads the session back and refuses on your behalf, rather than passing and leaving the failure for the first person to ask a question.
- Enable the source. Only a probe that returned and passed will let you, and the semantic MCP will not query a source that is not enabled — so a source that says connected is one that answered, from this installation, with the credential the row holds now.
- Ask a question the source answers, and check that the answer names it.
If questions still fail, the semantic MCP's log names which state it is in: no row, a row that is not enabled, a stored connection it cannot use (and why), or a statement the warehouse refused.
Rotating the key
Replace the credential on the source in the admin page, and probe again. That is the whole procedure: the connection is read per question, so the next question uses the new key with no restart and no redeploy. There is no Secret to patch and no pod to roll.
Test a manifest before you register it
The Test button on a source's Manifest tab tests a source that is already registered. To test a document before registering anything, call the API directly, as a super admin:
POST /api/admin/manifest/test
{"manifest": { … }, "credential": { … }, "parameters": { … }}
manifestis the document (or sendmanifest_yamlwith its text).credentialcarries the same fieldsPOST /api/admin/sources/<id>/credentialtakes (secret, orusernameandsecret, orprivate_key_pem). Leave it out entirely when the document'sconnection.auth.typeisnone; sending one with such a document is refused.parametersare the values the document'sconnection.parametersdeclare.
Nothing is stored: no source is created and no credential is written. The
request is tested against the document's own connection, and the credential is
sealed with this installation's ENCRYPTION_KEY before it leaves the app, so
the route answers 503 on an install without one. The response carries the
verdict under result — whether the document validates, and
whether each stream or column it declares resolved.
A source Prism reads out of an Oracle database
The same shape as the Snowflake section above, on the other engine: an Oracle
source is queried in place. Nothing is copied. A question over it is
compiled into one SELECT against one view and sent to your database over a
read-only session, and the answer is built from what comes back. Your database
sees one statement per question, bounded by the source's statement timeout, and
nothing else. (Until 0.12 this section described the opposite: a whole copy of
the view every week, kept for two years. That shape still exists — see If you
want the plan's history below — but it is no longer what an Oracle view means
by default, because a rolling plan nobody asked to keep history of does not
warrant a hundred copies of itself.)
Read this the same way as the Snowflake section: skip it unless a database source appears in your recipe list under Admin → Context streams. Which sources are offered is fixed by the release you installed, and nothing here is something you can add yourself.
Writing the document itself — which view, which columns, what each one means — is Read an Oracle view as a context stream. This section is the connection underneath it: what your DBA prepares, what to enter, and what the probe proves.
One thing an Oracle source does that a Snowflake one cannot yet: when it is your organizational context stream, per-person answers resolve through it. The ingest job reads the two identity columns the recipe names — the employee id and the address — out of the view, about once a day, and compiles the identity crosswalk from them. Two columns per person are held in Prism for that purpose; the plan itself is not. Questions scoped to a leader's organisation ("everyone who reports to X") still need a landed directory — an uploaded staff list, or a snapshot instance of the same view — and say so when refused.
What your DBA prepares, and it is the long pole
The ingest side is finished: the Oracle driver ships inside the appliance, so there is nothing to install, enable or mirror to read your view. So is the admin page's probe — the app carries that same driver, so an Oracle source is probed, approved and enabled from the screen like any other. Proving it below says what a passing probe does and does not tell you, which is the part worth reading before you plan around it.
Everything on your side of the connection is ordinary, and it is worth starting now because it is the part with a lead time:
| What | Why it is asked for |
|---|---|
| A view, not a table | the source reads exactly one view. What is in it is what Prism can ever see, which makes the view itself the access control |
| A local database user for the appliance | one service account for the whole appliance, never one per person. It appears in your audit trail as itself |
SELECT on that view, and nothing else | the grant is the control. Prism opens its walk with SET TRANSACTION READ ONLY, but that blocks DML and not DDL, so it is defence in depth over a read-only grant and never a substitute for one |
| A row identity column, if the view can carry one | asked for so a fan-out on a join is detectable; a live query needs no key to run, so a view without one still connects. A last-updated timestamp is no longer asked for — nothing is copied, so nothing needs to know what changed |
| Host, port and service name, and a network path to the listener | one Oracle Net connection, outbound from the Prism namespace to your listener's port |
Two limits that are worth knowing before your DBA plans around them, because both are properties of the driver rather than settings we chose to leave off:
- No
sqlnet.oraof ours is read, and there is nooraaccess.xml. An estate that keeps its connection policy in those files does not get it applied here; the connection is exactly the parameters below. In thin mode — the default — the driver has no notion of those files at all. In thick mode (below) the Oracle client library would read asqlnet.oraif one were placed on its path, and Prism's images ship none, so the effect is the same: the connection is the parameters, plus whatever your server requires and the client agrees to. - Wallets are PEM or nothing. A
cwallet.ssoauto-login wallet cannot be used, in either mode: the appliance has nowhere to tell it about one. If your policy mandates one, say so before the account is created — it is a real finding and it changes what we ship, rather than a value to set.
Neither of these is a workaround waiting to be found. Tell us early and the answer is a different build, not a different value.
If your database requires Native Network Encryption, that is not one of
those findings — it is a value. SQLNET.ENCRYPTION_SERVER = REQUIRED (with or
without SQLNET.CRYPTO_CHECKSUM_SERVER) refuses every connection Prism's
default driver mode makes, before a session exists, and the source's verdict
says so by name. The fix is one chart value, oracle.mode: thick, and no new
build: see
When a database requires Native Network Encryption.
What to enter, on the Oracle source
Sign in as a super admin, open Admin → Context streams, pick the database source from the recipe list, and give it its connection parameters and its credential.
| Parameter | What it is |
|---|---|
db_host | the hostname of your Oracle listener |
db_port | the listener's port — 1521 unless your estate moved it |
db_service | the service name of the database, not an SID and not a TNS alias. tnsnames.ora is not consulted, so an alias that works from your DBA's laptop is not a value that works here |
db_user | the service account the view is granted to. It is configuration, not a secret: it appears in your own audit trail and in every authentication failure, and somebody has to be able to read it to fix one |
The credential is that account's password, entered on the source in the
admin page. It is encrypted with the installation's ENCRYPTION_KEY and read
back in exactly two places: by the app, when an admin probes the source, and by
the ingest job on its schedule. Nowhere else — and it is never returned by the
API, never written to a log, and never carried in a probe verdict.
There is no schema parameter, deliberately. Either create the view in the service account's own default schema, or give the account a synonym for it. A schema name is the one part of this that differs per customer in a way a shipped recipe cannot carry, and a value nobody remembers to set is a worse failure than a synonym somebody has to create once. If your estate genuinely cannot do either, that is worth telling us — it is a small additive change and not a redesign.
Proving it
Sign in as a super admin, open Admin → Context streams, pick the Oracle source from the recipe list, enter the four connection values and the password, then press Test. It opens a real Oracle Net session to your listener, from the appliance, with the credential the row holds now — and then reports what that session established.
One word on this screen, whatever it runs: Test. What that means for a source depends on its kind — an Oracle session here, a signed-in request to an API for a source Prism copies, a compiled and explained statement for one it reads in place, a parse of the file for a roster — and the screen says which it did. The button used to be named after the mechanism, differently in four places, and the mechanism is not the thing you are trying to find out.
Storing the password IS a test, and a test that fails refuses the store.
The button on that screen is Store and test, and there is one credential on
the row rather than a copy each side, so the two steps are one request: Prism
opens the session with the password you just typed, and only a verdict it is
willing to stand behind gets written. A password your database rejects
(ORA-01017) is not stored and whatever was there before is untouched — which
is the behaviour you want, and it is the same for every source type. But a probe
that cannot reach your listener at all refuses the store for the same reason:
a network path not yet open, a listener that accepted and closed, or the timeout
are all "no verdict", and no verdict means no write. So an Oracle password cannot
be parked on the row before the egress exists. Two ways round it, and the first
is usually right: prove the egress first and then store the password
once, or — if your change windows force the credential to land before the
firewall does — store it without testing it: Save without testing on the
source's Credential tab, or POST /api/admin/sources/<id>/credential with
{"probe": false} in the body. Then press Test on the source once the path
is open. Either way nothing reaches
enabled until a probe has actually passed: an unprobed credential records
not_probed, which enable refuses. Rotating a credential
is the same request and the same rule.
Then, and this step is easy to miss because nothing prompts for it: approve
the source, and only then enable it. A source moves registered → probed → approved → enabled, and enable is refused from probed. Approving is you
saying you have read the manifest; enabling is you saying it may answer.
A passing probe means these things and no more:
- the listener was reachable;
- the credential was not rejected;
- and — only where the source names a schema — the session Prism got is the one it asked for: the schema the session actually ended up in is read back out of it and compared with the one asked for. Where the source names no schema there is nothing to compare, so the verdict makes two claims rather than three, and the session it established is reported without being vouched for.
That third one is worth its own sentence, because a login that authenticates
perfectly well can still land somewhere other than where you meant: a login
whose default schema is not the one the walk expects sees none of its tables. So
Prism compares rather than assumes, and a session in a different schema is a
refusal naming the field — it cannot buy enabled. The comparison folds case,
because the schema travels unquoted and Oracle answers PRISM where the
connection asked for prism, and refusing a correctly configured install would
be worse than the fault it replaced.
The service name is reported, not compared, and that is deliberate. Your
listener refuses a service name it does not know before there is a session at
all, so there is nothing for a comparison to catch — and there is something it
would wrongly catch. A service created the documented way,
DBMS_SERVICE.CREATE_SERVICE(service_name => 'svc840', network_name => 'svc840net.example.com'), is registered with the listener under its network
name, so that is the value db_service must carry; inside the session Oracle
answers the internal svc840. Comparing those two would refuse a correct
install with no value you could type to get past it. So the verdict shows you
the service the database says you are on — useful on a host running two of them
— and lets you judge it.
A passing probe does not mean your view exists, that the account can read it, or that any column you named is real. The probe names no object of yours at all — it asks Oracle about the session and stops there. The Test on the source's Manifest tab is what finds those out: it compiles every column the recipe names into a statement, has Oracle parse each one against your view, runs each with no rows returned, and reports every column that did not resolve with the view's real column list beside it — before the source can be enabled. That is the check to run once the view is cut, and again after any change to it.
A refusal names the engine's own code, and that code is the thing to put in a ticket:
ORA-01017— the database rejected this installation's credential: the password, the login name, or the account's ability to create a session at all. Nothing about the network is wrong. On an installation running in thick mode this code has a second cause, and the verdict says so: Oracle Instant Client refuses a password longer than 30 bytes as though it were wrong. A password longer than 30 bytes is how to tell the two apart before you rotate anything.- a service name the listener does not know arrives as
DPY-6005withDPY-6001inside it — the listener answered and does not serve that name. An SID or atnsnames.oraalias entered asdb_servicelands here, because neither is resolved. - nothing answered at that host and port — the listener is down, the port is wrong, or the network path is not open. Prove the egress is the check that separates those from a credential fault.
- the listener accepted and closed —
DPY-4011, and it comes back in milliseconds rather than at the timeout. Something took the connection and hung up without giving a session: node validation on the listener (TCP.VALIDNODE_CHECKINGwith the Prism pod's address not inTCP.INVITED_NODES), a TLS-only (tcps) listener dialled in clear, or a firewall or proxy in the path. It is neither a credential fault nor a timeout, and the fix is on your listener or your network rather than in this screen. - the timeout — the probe bounds itself, so an address that swallows packets comes back in seconds instead of hanging the screen.
Oracle's own message never travels into the verdict: it quotes the statement, the schema and sometimes the service name, so what you get is the code, the address dialled, the login and the elapsed time, deliberately and no more.
What has not changed, and it is still the long pole by a wide margin: prepare the database side now. The view, the service account, the grant, the network path and the answers to the two thin-mode questions above are the same whatever the screen does, and no probe verdict shortens any of them.
The sequence, then, is the ordinary one:
- Probe the source and read the verdict. It reports what the session actually established, and it is where a wrong service name, a rejected password or a blocked network path is named as itself.
- Approve the source, then enable it, and let one scheduled run complete.
- Check the row count against what your DBA sees in the view. This is the check worth doing by hand once: a credential that authenticates cleanly and sees an empty view is the failure this whole procedure is arranged around, and from the chat it is indistinguishable from a working product with nothing to say.
And enabled is not verified. Enabling an Oracle source means this
installation demonstrably opened a session to your database. Whether the figures
it copies are right is step 3 and the questions you ask afterwards, and it is
not something the appliance can decide.
Rotating the password
Replace the credential on the source and probe again. The next scheduled run uses it. As with a warehouse, there is no Secret to patch and no pod to roll.
If you want the plan's history
A live query answers for the view as it stands now. If you want to ask how the plan changed between March and June, the stream has to keep dated copies — which means running it as a snapshot rather than reading it live.
Change the kind on the stream you already have. On registration, open the
recipe in the editor, set kind: snapshot and give it an ingest block with a
query stream and a deliberately short keep (a quarter of weekly copies is
P90D). The ingest job then takes a dated copy of the view on that schedule and
keeps that many.
Read an Oracle view as a context stream
carries that whole document for the recipe's example view, ready to adapt.
A document you edited is walked like any other. The ingest job walks the document the stream holds, under whatever name you gave it. That includes one you wrote, pasted, or changed in the editor before registering it. It checks only that the document names nothing this release does not implement.
Store the credential on the source. The ingest job never lends its own configuration to a document of your own: the document chooses where its requests go, so the job does not hand it a secret it holds for another stream. A stream with no credential stored on it wears not ingested on Admin → Context streams until you enter one, and the self test's ingest dispatch line names it. Connection settings are covered the same way: a document of your own never reads them from the job's environment, whatever it says.
A document that names a part this release does not implement wears not ingested with the part named, and no edit on your side supplies it. Ask us.
On 0.15.x and earlier, a stream carrying an edited document under a name of its own is never walked, so keep the name the recipe ships with there. Do not rename an existing stream to a prebuilt name to work around it: the name is usually taken, and where it is free the prebuilt document takes over the walk and your edits stop being used.
Size it before you choose, because the multiplier is the schedule and not the view: each copy is kept whole, so a weekly stream kept two years is about a hundred and five copies of the whole plan. One copy of a 2.6-million-row view is roughly 1.2 GiB, which puts that shape north of 100 GiB and a monthly stream kept a year at about 16. Decide the pair — how often, and how long — against the volume you are willing to give it, and expect the pre-flight to warn you: the warning starts at five copies, so every realistic shape trips it.
Only one stream can hold the organizational designation.