Ask: an assistant that answers from your documents
Ask puts a chat panel on every page of a site. A reader types a question, the assistant reads the documents in the repository and answers with links to the pages it drew from. It never answers from anything outside the corpus you declare, and it never invents a link: every citation is a page the site actually serves.
Two people switch it on, and neither can do it alone:
- An author declares what the assistant may read, with an
ask:block in a.stele.yml. - A site owner turns Ask on for the site in the portal, acknowledges that repository text will be sent to an AI provider, and picks which provider and model answer.
A manifest cannot name a provider or model, and the portal cannot widen what the manifest declared. That split is deliberate: the repository says what the documents are, the portal says what is allowed to read them.
The short version
- Add an
ask:block with anameto the.stele.ymlat your repo root (or in a subdirectory, see below). Push. - In the portal, open the site, find the Ask card, tick the acknowledgement and click Enable Ask for this site.
- Check the provider and model under Provider and model. With one provider registered it is chosen for you; with more, pick one.
- Click Show what the assistant can read on the corpus row and look down the list. Anything you would not want quoted back goes into
ask.exclude. - Open the site. The panel is in the bottom corner of every page.
The ask: block
ask:
name: Platform Docs # required — shown to readers in the chat panel
audience: internal # internal (default) | invited | all
prompt: | # optional guidance for the assistant
Prefer the runbook wording where two documents disagree.
sources: # optional: extra filetypes to make readable
- "**/*.likec4"
exclude: # optional: paths the assistant must never read
- internal-notes/
renders: # optional: data files and the page that renders each
technical-audit/findings.json:
page: technical-audit/index.html
anchor_field: id
| Field | Default | What it does |
|---|---|---|
name |
required | The name readers see in the panel. Name it after the documents, not the repository: "Support Docs" reads better than "support-site". At most 80 characters. |
audience |
internal |
Who may ask, named as a set of people like the site's access. internal is everyone at Axelerant. invited adds the people on the site's viewer list, and still refuses share-link-only viewers. all is anyone who can read the site, share-link viewers included. Anything beyond internal needs a provider approved for customer data. See Who may ask. |
prompt |
none | Guidance for the assistant: house style, which document wins a disagreement, what to point people at. It cannot override the citation rules. At most 2,000 characters; see Limits on name and prompt. |
sources |
text filetypes | Extra glob patterns to make readable. Use it for source formats the site does not publish, such as .likec4 models. |
exclude |
none | Paths the assistant must not read at all. |
renders |
none | Data file to the page that renders it, both relative to the declaring directory. See "Where citations point". |
The only required field is name. Everything else has a sensible default.
One block per corpus, and where it lives
An ask: block declares a corpus rooted at the directory of the .stele.yml that holds it. A block in the root manifest covers the whole repository; a block in support/.stele.yml covers support/ and nothing above it.
When more than one block applies, the nearest one wins for a given page. A reader on a page under support/ talks to the support/ corpus; a reader anywhere else talks to the root corpus. A child corpus scopes to its own subtree, and the parent still covers everything outside it.
What merges and what does not:
promptconcatenates, ancestor first. The root prompt is read before the child's, so a child adds to its parent's guidance rather than replacing it.audience,sourcesandexcludecome from the nearest block only. They are not merged with a parent's. A child that narrows has to actually narrow, which is why it does not inherit its parent's file set.nameis per corpus. The panel shows the name of whichever corpus the current page belongs to.
Limits on name and prompt
prompt rides on every question, after Stele's own rules for the assistant, so its length is capped:
- 2,000 characters per block.
- 4,000 characters for a corpus's whole prompt, once its ancestors' prompts are joined in front of its own. Because the ancestors come first, it is the end of the nearest block that gets cut.
- 80 characters for
name.
Over a cap, the text is cut short rather than refused, and the sync shows a warning on the site's Ask card naming the .stele.yml to edit. The rest of the manifest keeps working. If your guidance does not fit, move the detail into a document the assistant can read and point at it from the prompt.
To see the prompt exactly as the assistant gets it, open the site's Ask card in the portal and click Show the guidance the assistant is given on a corpus row.
What the assistant can read
An ask: block makes all text under its directory readable, including files the site does not publish. That is deliberate. A build: step replaces a directory's sources with its output in the published tree, so the sources that most need reading (a .likec4 model, say) are exactly the ones the site does not show.
Four ceilings always apply and cannot be configured away:
- The corpus root. Nothing outside the declaring directory is ever readable.
.gitignore. Anything a.gitignorepattern matches is not read, whether the file is the repository root's or one in a nested directory: each governs its own directory and everything below it, and the nearest file with a matching pattern decides, as in Git. That includes abuild:step's output when you gitignore it, as the build guide recommends; the assistant reads the sources instead, which is the point.- Hidden directories.
.foo/is never read. - A secret denylist. Files whose names say they hold credentials are refused regardless of every other setting,
sourcesincluded:.env*, key and certificate material (*.pem,*.key,*.p12,*.pfx,*.ppk), SSH key pairs (id_rsa,id_ed25519and the other default names,.pubincluded), keystores (*.jks,*.keystore), PGP files (*.gpg,*.pgp,*.asc), VPN profiles (*.ovpn), Terraform state and variable files (*.tfstate*,*.tfvars*),htpasswd,kubeconfig, service-account JSON, anything withcredentialorsecretin its name, and a file withtokenorpasswordin its name when it is extensionless or a.txt,.csv,.json,.ymlor.yamlfile. A page or source file named for a token or password (tokens.md,reset-password.md,token-landing.ts) is ordinary documentation and stays readable; a design-tokenstokens.jsonis the known cost, and renaming it is the fix. The list is by name, so it catches the files that conventionally hold secrets, not a secret pasted into a page; the people who can commit to the repository are trusted not to do that. When the denylist is what keeps a file out, the sync records a warning on the site's Ask card naming it, so you can tell why the assistant never reads it, and notice a key committed by mistake.
Within those ceilings, sources widens the readable set and exclude narrows it. HTML pages are read as Markdown extracted from the page, not as raw markup, so an answer quotes prose rather than tags.
What a reader cannot see, the assistant does not read either:
- HTML comments are removed from Markdown and HTML files alike. Comment syntax inside a code span or code block is shown on the page, so it stays.
- Hidden elements are dropped: anything with the
hiddenattribute,aria-hidden="true", or an inlinestyleofdisplay: noneorvisibility: hidden. That holds for an HTML page and for raw HTML written in a Markdown file, including a<div hidden>wrapped around whole Markdown paragraphs. - Elements a page never shows are dropped:
<template>,<style>,<noscript>, and<script>except a JSON data block, from an HTML page and from raw HTML in a Markdown file alike. So are<noembed>,<noframes>and a<title>in the body, and the text between the tags of an<iframe>,<object>or<canvas>, which the page never shows; the frame's own tag stays. From an HTML page, navigation and embeds are dropped too; in a Markdown file a<nav>is content you wrote and the page shows, so it stays. - Kept on purpose: text hidden by a CSS class such as
sr-only,altandtitletext, and the inside of a closed<details>. A screen reader or a click shows those to a person. Hiding done in a stylesheet is kept too, since extraction applies no CSS.
Readable is wider than citable. The assistant may use an unpublished file, and will say the passage came from repository source rather than dressing it up as a page, but it can only link to URLs the site serves.
Check what it can see
Before you switch Ask on, and before you ever set audience: all, look at the list. In the portal, the site's Ask card lists each corpus the manifest declares. Click Show what the assistant can read on a corpus row and you get every readable file, with:
- its size and an estimate of how many tokens it would cost to read in full;
- an unpublished mark on files the assistant can read but the site does not serve;
- an extracted mark on HTML pages, meaning the assistant reads the Markdown extracted from them;
- not readable on files the assistant will list but cannot open;
- how many HTML comments and hidden elements were removed from a file, and from the whole corpus, so the text it reads differing from the file you committed is never a surprise;
- the URL it would be cited as, if any;
- a show text link that displays exactly the text the assistant would read.
If you see an internal note, a draft, or a data export you would not want quoted back, add it to ask.exclude, push, and check the list again after the sync. The list is read from the last sync, so a manifest change shows up only after the site has synced.
An ask.exclude pattern that matches nothing excludes nothing, and that is almost always a typo or a renamed folder. The sync still succeeds, and the site's Ask card shows a warning naming the entry (ask.exclude[2]) and the .stele.yml it is in. Patterns are relative to the directory of that .stele.yml, and for a directory with a build: step they are matched against the path the build wrote on disk (dist/404.html), not the rebased path the file browser shows. A pattern that names a whole directory counts as matched when the directory exists, even if nothing under it was read.
Who may ask: audience
audience names a set of people, the same sets the site's access uses: internal is everyone at Axelerant, invited adds the people on the site's viewer list, and all is everyone who can read the site. The default is internal: only people signed in with an Axelerant Google account can ask, whatever the site's access setting. A site shared with a client by link renders for them as usual, but the panel refuses their questions until the corpus says otherwise.
audience |
Everyone at Axelerant (Google sign-in) | Invited by email | Share link only |
|---|---|---|---|
internal (default) |
yes | no | no |
invited |
yes | yes | no |
all |
yes | yes | yes |
Like every access setting, the audience is intersected with the others, never added to them: someone can ask only if the site's access, each folder's access: and the audience all admit them, and the assistant never answers from pages the asker cannot open (see How access settings combine). So all on a site set to Only invited people still means only the listed people, and a client folder marked restricted stays out of answers for anyone it does not admit.
Use invited to give a client the assistant without giving it to everyone who has the link: invite them by email in the site's Viewers card (see Sharing & Access), and they can ask once they have signed in by the emailed link. Use all only when anyone holding the share link should be able to ask.
To open Ask beyond staff, set audience: invited or all and have a site owner enable Ask in the portal, on a provider approved for customer data. Both are required because each side knows something the other does not: the author knows whether the documents are fit to be questioned by an outsider, and the site owner knows whether the AI provider is approved for that customer's data. A corpus beyond internal on an unapproved provider shows an error on the Ask card and does not answer. Enforcement happens at the endpoint, not by hiding the panel.
Before setting audience: invited or all, read the corpus list above with the client in mind. Everything readable is answerable.
Where citations point
Every readable file is given, at sync time, the one URL an answer may cite it as. The assistant never builds a link itself. The rule, in order:
- A page you declared renders it (
ask.renders): cited as that page. Honoured only when the page is actually served. - The file is a page of its own (
.md,.html,.tsx): cited at its own URL. Markdown and HTML pages also carry their headings, so an answer can link to a section. - The file is a source under a
build:directory: cited as that directory's page, because the rendered form of that source is that page. - Otherwise, the nearest folder above it with a landing page; and if there is none, the file is readable but not citable.
Three cases you will meet:
A Markdown or HTML page. Nothing to declare. guides/deploy.md cites as /guides/deploy, and a claim from its "Rollback" section can cite /guides/deploy#rollback. For HTML, only headings with an authored id get anchors.
A LikeC4 or D2 model. The sources are not published because the build output replaced them. Make them readable with sources, and they cite the diagram page automatically:
# architecture/c4/.stele.yml
build:
command: likec4 build . --output-single-file
output: dist/
ask:
name: Architecture
sources:
- "**/*.likec4"
An answer that read architecture/c4/model.likec4 links to /architecture/c4/, the rendered diagram. No renders entry is needed; the build: step already says which page the sources become.
A data file a page draws. A JSON, CSV or YAML file the site serves is, by rule 2, a page of its own, and its URL opens the raw file. If a page renders it (a findings register, an inventory, a glossary), say so:
# support/.stele.yml
ask:
name: Support
renders:
technical-audit/findings.json:
page: technical-audit/index.html
anchor_field: id
Now a claim drawn from findings.json links to /support/technical-audit/index, the page a reader can actually read. The data file is still read as written; this changes where the link goes, not what the assistant sees.
anchor_field is optional. It names the record field the page uses as each record's element id, so the assistant can link a claim to one record, /support/technical-audit/index#TA-001. Declare it only if the page really emits those ids (for instance <article id="TA-001">). A link to an anchor the page does not have scrolls nowhere, which is worse than a link to the page.
What anchor_field needs from the JSON. Record outlines and per-record anchors are computed for .json files only; a CSV or YAML entry in renders still cites as the page, but as a whole. The records are the top-level array when the document is an array of objects, otherwise the largest array of objects among the top-level keys, so { "meta": {...}, "data": [...] } works and deeper nesting does not. Records need not share a structure: each is checked on its own, and one that lacks the anchor field, or holds a value that is not a string or number, is skipped without a heading or an anchor. The outline line is the anchor value followed by the first of title, name, summary, label or description that is a non-empty string, so a file whose records carry one of those fields reads better in list_files. If no record array is found the file gets no outline and cites as the whole page; if the JSON does not parse, the sync records a warning on the site's Ask card and the file stays readable. Neither case fails the sync.
Both paths in renders are relative to the directory of the .stele.yml that declares them, and neither may be absolute or contain ... If renders names a file the corpus lacks or a page the site does not serve, the sync records a warning on the site's Ask card and the file cites by the ordinary rules.
What the site owner sets in the portal
Everything below lives on the site's Ask card in the portal, and none of it can be expressed in YAML.
- Enable Ask. A one-time acknowledgement that the repository's text, and the questions readers type, go to the assigned AI provider. It records who agreed and when. Turn Ask off stops every question on the site immediately; earlier conversations stay readable until they age out.
- Provider and model. Which registered provider answers, and which of its allowed models. With exactly one provider registered it is selected automatically and the card says so. A provider marked Customer data has been approved as a subprocessor; one marked Internal data only has not, and should not answer a site shared with a client. Every change of provider or model, every change to a provider itself (its credentials, allowlist, enabled state or customer-data approval) and every time Ask is turned on or off is recorded with who did it, when, and the values before and after. Anyone who can see the AI provider registry can read that record: the newest changes on the AI Providers page, and each provider's full record under History on its page.
- Limits. A daily token budget for the site and a questions-per-viewer-per-hour rate. Blank uses the platform default; zero stops every question. When the budget is spent the panel says so and the page itself keeps rendering.
- Read conversations. Every conversation is stored for 90 days and readable by internal users, with the tool trace and token usage for each turn. The panel discloses this to readers. Reading what people ask is how the documents get fixed.
What readers see
The panel opens from a button in the bottom corner. A reader types a question and watches the answer stream in, with links to the pages it came from. They can widen the panel, open the conversation as a full page in a new tab, stop a running answer, rate an answer up or down, or start a new conversation. The conversation follows them from page to page within the tab.
Clicking a citation opens the page in the same tab and keeps the conversation, so a reader can check a source and come back to ask the next thing.
Costs
Each question makes several calls to the model: the assistant lists files, searches, reads, and then writes. Costs and token counts per conversation are on the site's Ask activity page in the portal, and the site card shows questions in the last week, reported cost in the last month, and how much of today's budget is spent. A typical question on a small corpus costs a fraction of a cent; a vague question on a large corpus of long HTML pages costs more, because the assistant reads more before it answers. Tight exclude lists and a clear prompt both help.
Some providers say what each question cost and some do not. OpenRouter does; AWS Bedrock, OpenAI and Anthropic do not. For those, Stele estimates the cost from the provider's published price per token, and shows it with a ~ (and "est." in a conversation) so you can tell it from a cost the provider reported. The estimate is the list price for the tokens used, so the bill can differ, for example after discounts or credits. A question asked of a model Stele has no price for has no cost at all, and the activity page then calls its total a minimum.
Troubleshooting
| What you see | Why | What to do |
|---|---|---|
| No panel on the page | No ask: block covers this page, or the site has not synced since you added one. |
Add the block or resync. Check the Ask card lists the corpus. |
| "Sign in to ask questions about this site." | You are a share-link or invited viewer and the corpus does not admit you. | Set audience: invited (invited viewers) or all (share-link viewers too), and have the site owner confirm Ask is enabled. |
| The panel says Ask is not enabled | The manifest declares a corpus but nobody has acknowledged the opt-in. | A site owner enables it on the Ask card. |
| Answers cite the wrong page for a data file | No renders entry, so the file cites at its raw URL. |
Declare renders for it. |
| A corpus row shows a warning | The sync found a problem: a renders path outside the directory, a page that is not served, an exclude pattern that matches nothing, a file the secret denylist refused, a disabled provider. |
The warning says which; fix and resync. A denylisted file that holds a secret should leave the repository; one that merely looks like a secret needs a new name. |
| The assistant quotes something it should not | The file is under the corpus root and not excluded. | Add it to ask.exclude, push, and check the corpus list again. |
Related
- Configuration Reference for the rest of
.stele.yml - Sharing & Access for what a share-link viewer can and cannot do
stele-resources/references/manifest.mdfor the exact field reference, and Give your agent the Stele skill to install it as an agent skill