Pages

A page is a markdown document that shows records from your collections: a table of orders, one customer’s record, how many leads came in. You write it on Pages in the console, preview it as you type, and publish it. A page is a record published like a loop: you edit a draft, and readers see the published version until you publish the next one.

Members and admins read pages in the console, and a published page can also be placed on your own site in a frame (see Put a page on your site), where a block can hold a loop’s chat or a form a reader adds a record with (see Let readers add a record).

Write a page

  1. Open Pages and press New page.
  2. Under Start from, keep A blank page, or choose one of the starter pages.
  3. Enter a Name, such as “Order status”. A blank page is headed by it as you typed it. A starter fills in its own name until you type one. The page’s short name, such as order_status, is made from it and shown under it; it names the page in its address and never changes. To choose another, open More. A short name your account already has gets a number, such as order_status_2.
  4. Press Make the page. A blank page lists your account’s first collection, if it has one; a starter is its own page under your name. The page is saved, not published, and its screen opens with its preview. Publish it (see Publish), or first press Change it yourself to edit its text. You can also ask LoopAssistant for changes on its screen, before or after you publish it (see Change a published page).

A page that is made can still warn: on its screen, it lists the same warnings the publish card shows (see Publish) until you press Done.

If the page is refused, the screen names each problem, and Change it yourself opens the editor on its text. The editor’s preview follows your typing, and a key the page does not have, or a value it does not take, is marked as you type. The save checks in two steps. First it checks the document itself, and names every problem there at once, each at its template line or at the key it is about, with the fix. Only when the document passes does it check the blocks against your account, so a second save can find problems that the first did not name.

Ask LoopAssistant for a page

You can also ask LoopAssistant for a page, for example “a page that shows my leads to my team”. LoopAssistant asks who reads the page and what it shows, and then proposes it on a card. The card shows who may read the page, each collection it reads, and what each block shows.

  • Approve saves the page as a draft. Nothing is published yet.
  • LoopAssistant then asks to publish it, with the Pages screen’s own Publish card in the chat, with what the draft changes against the version readers see now. Pressing Publish publishes the draft in your name, with the same checks; Decline leaves it a draft. If the draft was saved again after LoopAssistant asked, the card says so and does not publish it.
  • You can also open Pages to read the draft, and publish it there as any other draft.

Until you publish a page it proposed, LoopAssistant can revise that page’s draft in the same conversation.

Change a page with LoopAssistant

You can ask LoopAssistant to change a page your account already has, for example “add a Done button to my tasks page”. LoopAssistant reads the page as it is published, and then proposes the whole next version with only what you asked changed. The card asks “Save a change to “Tasks” as draft v3?” and shows what changes against the version readers see:

  • each other change in a sentence, for example who may read the page or a collection it reads,
  • the template line by line. Each changed line says Removed: or Added:, and a changed line reads as the old line removed and the new line added, with only the words that change marked: struck through on the removed line and underlined on the added one. Long lines wrap, so the change shows on a phone too. One unchanged line stays beside each change, and the rest are folded with their count. A line that is one block, such as {{collection orders}}, reads as what the block shows (“A table of Orders …”), with its text under As written; a block that replaces another says Changed:, and As written shows the old and the new text.

The card leads with the change. Press Show the whole page to read the whole page as it will be, with who may read it, each collection it reads, and the preview.

Approve saves the change as the page’s next draft. Readers keep the published version until you publish the draft on the Pages screen, and the publish card shows the same change.

When a change is already waiting, even on a page you never published, LoopAssistant’s change starts from that draft, yours or another run’s: the card shows what this request changes in it, and Approve replaces it. So you can adjust a new page as often as you like before anyone sees it. If the draft was saved again after the card was drawn, or the page was published again after LoopAssistant read it, nothing is saved and LoopAssistant reads the page again.

LoopAssistant proposes pages for members, admins, anonymous visitors and invited readers. A page for visitors comes with its limits on what one address may do, and with its own public address when you want a link to send. A page for invited readers names the addresses or domains it invites, and a change to it keeps them. A page for signed-in users of your own site is set up on the Pages screen, because it needs a key you set there.

When the page itself is where its records come from, such as a feedback form, LoopAssistant can create the collection with it. The card names that on its own line, for example “Creates collection feedback with fields message, written by anonymous.”, and approving the card creates the collection as the Collections page would. A field with fixed values is named with them: “Creates collection tasks with fields title, status (open, doing, done), written by member.”. A name your account already has is never written over. Otherwise, if LoopAssistant names a collection your account does not have, in any block or form, the card is refused with the collections you do have, and LoopAssistant proposes again with one of them or creates the collection with the page.

If you ask for a loop and a page that shows what the loop keeps, LoopAssistant proposes the loop first. The loop’s collections exist once you approve its card, so the page’s card comes after that. If LoopAssistant does not propose the page, it says so, and you can make the page on the Pages screen.

Starter pages

Four starter pages show the common shapes. Each one reads one collection, and the FAQ also embeds a loop’s chat: change each name to one your account has.

StarterWho reads itWhat it showsWhat it needs
TasksMembers and adminsA board of open, doing and done tasks by due date, each card with Start, Done, Reopen, Edit and Delete, a task’s own view when you open it, a search, a chart of how many tasks are in each status, a calendar of tasks by due date, and a form that adds a task.A collection tasks with title, status (with the choices open, doing and done), owner and due, whose rows members and admins may change and remove.
Send us a requestAnyone, at its own addressA form that leaves a request or feedback, with a file if they have one, and no sign-in. Each request emails the account’s owners.A collection requests with name, email, message and a file field attachment.
PeopleMembers and adminsYour team’s people, as cards, with a search, and a form that adds a person, which only admins see.A collection people keyed by email, with name, role and phone.
Questions and answersAnyone, at its own addressAnswers from a collection, a form that adds one, which only an admin sees when they open the page in the console, and a chat with one of your loops.A collection articles keyed by question, with answer, and a published loop help for visitors.

Choose one under Start from on New page. The screen says whether your account has the collection the starter reads. If it does not, Make the page makes it first: the same card as on Collections shows what it creates, nothing is created until you press Create, and then the page is made. Or rename the collection to one you have, with Change it yourself. A collection whose key is made for each record (tasks and requests) lets a form save a new record every time. Then change the readers if you want others, save and publish.

LoopAssistant starts a page you ask it for from the nearest starter, for your team or for visitors. For a page where visitors leave feedback, it starts from Send us a request, and it can create the collection on the same card (see Ask LoopAssistant for a page). A page for visitors needs an identity block in its access, as a loop’s does. The two visitor starters’ documents:

Send us a request:

# A starter page: a public form where anyone leaves a request or feedback, with
# a file if they have one, and no sign-in. It assumes a `requests` collection with
# `name`, `email`, `message` and a file field `attachment`; name yours. Each
# request emails the account's owners, and your team reads it on Collections.
page_id: request_form
title: Send us a request
access:
  readers: [anonymous]
  identity: {kind: anonymous, limits: {per_ip: 30/1m, monthly_writes: 500}}
delivery: {public_path: true}
collections:
  - {name: requests, read: [admin], write: [anonymous]}
template: |
  # Send us a request

  Tell us what you need. We read every request.

  {{form requests fields=[name, email, message, attachment] email=owners}}

Questions and answers:

# A starter page: public answers, and a chat for a question they do not
# answer. It assumes an `articles` collection with `question` and `answer`, and
# a published loop named `help` that signs visitors in as anonymous; name yours.
# An admin who opens it in the console sees a form that adds an answer; visitors
# never do.
page_id: faq
title: Questions and answers
access:
  readers: [anonymous, admin]
  identity: {kind: anonymous, limits: {per_ip: 30/1m}}
delivery: {public_path: true}
collections:
  - {name: articles, read: [anonymous, admin], write: [admin]}
loops: [help]
template: |
  # Questions and answers

  {{collection articles fields=[question, answer] view=list sort=question}}

  {{form articles fields=[question, answer]}}

  ## Ask us

  Not answered here? Ask your question in the chat below.

  {{loop help}}

The page document

page_id: order_status
title: Order status
access:
  readers: [member, admin]
collections:
  - {name: orders, read: [member, admin]}
template: |
  # Order status

  Orders so far: {{count orders}}. The latest:

  {{collection orders fields=[number, customer, status] limit=20}}
KeyWhat it says
page_idThe page’s id. It must be the id you gave the page.
titleThe title a reader sees in the page list.
accessreaders: who may read the page, any of member and admin. A page with its own address may also name anonymous, or invited with the people it invites (see Open a page to the people you invite).
collectionsEach collection the page’s blocks may show, and who may read it: read: [member, admin]. A block may show only a collection listed here.
collections … rowsOptional. Leave it out, and a reader sees every record of that collection they may read. Write rows: owner_field:<field> to show each reader who is not a member only the records whose <field> holds their own identity. To fill <field> with that identity when a loop writes the record, bind it to run_subject (see Record who started the run). A member or an admin always sees every record.
collections … writeOptional. Who may add a record to that collection through a {{form}} block: any of anonymous, user, invited, member and admin. Leave it out, and nobody can. See Let readers add a record.
collections … change, removeOptional. Who may change and who may remove rows from the page, and which: change: {by: [member], rows: own}, where rows is own (the rows the reader added) or any. Only within what the collection’s own access allows (see Who may change or remove rows from a page): the save refuses more, naming the collection’s setting. Leave both out for a page whose readers only add rows; by: [] is the same as leaving one out. See Let readers change and remove rows.
templateThe page itself: markdown, with blocks in {{ }}.

A document may also carry delivery and loops. delivery puts the page on your site (see Put a page on your site), and loops lists the loops whose chat a {{loop}} block shows. A console page needs neither.

Blocks

A block is one {{ }} in the template, on one line. It shows records from a collection the page lists, and it sits in the page where you write it.

BlockWhat it shows
{{collection orders}}The collection’s records: a table, or a list or cards with view=list or view=cards (the keys are below). The publish card names the view: “A table of”, “A list of” or “Cards of”.
{{record orders A-1042}}One record, by its id: the value of the collection’s key field.
{{record orders from=row}}The row a reader opens: each row of a {{collection orders}} block on the page links to it. See A row’s own view.
{{search orders fields=[number, note]}}A search box. A reader types words and finds the rows that hold every word in one of the listed fields (all the collection’s fields without fields=). It searches only the rows that reader may read, so it never finds a row the page hides from them, and it never searches a sealed field. It reads at most 500 rows: a reader who may read more is told “Searched the first 500 rows you can read”. It works with scripts off. It takes fields=, limit=, view=, where= and sort=, as a collection block does.
{{count orders}}How many records the collection holds. Above 500 it reads “500 or more”. It shows the number alone, so write the words before it, as in Orders so far: {{count orders}}: after it, “orders” reads wrong when there is one.
{{fields orders}}The collection’s declared fields and their types.
{{chart orders by=status}}A chart of the collection’s records: one bar for each value of the field, as long as how many records hold it. sum=total adds a number field instead of counting rows; view=line draws a line over a date field or by=@created_at (each day a row was added; @changed_at for the day it last changed), every day from the first to the last, a day with no records at zero (when every record falls on one day, that day draws as one bar, since one point is no line); where= takes the conditions a table’s does. Its title says what it counts and which rows: “Tasks by status”, “Sum of total by status”, “Tasks by status, where owner is ivy”. A choice field draws its values in their order, each one, none held or not; other values draw the largest first; records with no value draw as “(none)”. It draws at most 31 bars or days and says how many more it leaves out (on a line, the latest 31 days). Each reader’s chart counts only the records they may read. It reads at most 500 records: past that, it says each number is at least what it shows, and a sum= over a field that holds a number below zero is not drawn, because its total could be wrong either way. It reads with scripts off. A sum= of a field that is not a number, or a line over a field that is not a date, is refused at the save.
{{loop order_help}}The loop’s chat, on a page placed on your site. It runs as the page’s reader, so they sign in once. The loop must be one your account published (a loop Reactor ships, such as lead_qualification, opens on your page only as your own edited version; start from a demo on Home instead), listed under the page’s loops, and sign readers in the same way as the page: a save or a publish is refused when it signs them in another way. To let the chat change what its reader sees on the page, for their visit (write in a scratchpad, filter or refresh a table, or scroll to a block), give the loop client_navigate in its capabilities; its entry may name the tools, page_write, page_filter, page_refresh, page_focus and page_prefill. page_filter narrows a table by a field’s value, by a number range on a field that holds numbers (at_least, at_most), or by both on different fields, and the table says what it shows (“Showing 2 of 3 records where price is at most 55”). Nothing it does changes a record, and another reader of the page sees it unchanged. After each reply, the page reads its blocks again, so a table shows a record the chat just saved without a reload. The console’s preview says where the chat will show.
{{form waitlist fields=[email, company]}}A form that adds one record to the collection, with one input per listed field, checked against the field’s declared type. A reader may use it only if the page’s collections entry names them in write; an anonymous visitor also within the page’s per-address limit and its monthly cap. A form only adds: it never changes a saved record. After a save, the page’s blocks that show the collection update. Add email=owners or email=<address> to email someone at each save: see Email someone when a form saves. The console’s preview shows the form and does not save.
{{user name}}, {{user email}}The reader’s own name or email address, inside your words: Hi {{user name else=there}}, welcome back. Each reader sees only their own, filled in when they open the page. A member or admin reads the name and address on their account; an invited reader reads the address they signed in with, and has no name; a signed-in user of your site reads what your page’s claims carry. A reader with none reads the else= words (else=there, or else=[my friend] for more than one word), or nothing. Write {{user name}} with a space: {{user.name}} is refused at the save. The value is shown to the reader only and is never sent to a loop.
{{scratchpad answer}}Nothing, until a {{loop}} block’s chat writes into it for its reader: the loop’s answer laid out on the page, at most 4,000 characters, until the reader leaves.

A collection block takes the keys below, and a record block takes fields=:

KeyWhat it does
fields=[number, status]The columns to show, in this order. Without it, the block shows every declared field. Each column is headed by its field in words: pickup_date reads “Pickup date”. More fields are filled in by pio on every row: @created_at, headed Added, is the day the row was added; @created_by, headed Added by, is who added it; @changed_at, headed Changed, is the day the row was last changed; @changed_by, headed Changed by, is who changed it last. See Who added a row.
limit=20The most rows to show, from 1 to 100. The default is 100. When more records match, the table says “Showing the first 20 rows. More match.”
view=listHow the rows are laid out. view=table is the default. view=list shows each record as one line: its first field, then the others in a quieter line, each with its name (“Status: open”). view=cards shows each record as a card that lists its fields by name, in a grid that fits as many cards across as the screen has room for. Every view takes the same fields=, limit=, where= and sort=, and works with scripts off.
view=board group=statusA board: a column for each value of status, in the order the collection declares them; a column for each value a row holds that the field no longer lists, such as blocked, said as Not one of the Status values; and a column (none) for rows with no value. group= must name a field with declared choices, or the save refuses it and names the fields that have them. Each row is a card; a button from actions=, such as actions=[Done: status=done], moves its card to that column. The board needs status to place a card, so fields= may leave it out: the column already says it. On a phone the columns scroll sideways inside the board, never the page.
view=calendar by=dueA calendar: each row on the day its date field due holds. It opens on this month, with today marked, and Previous, Today and Next step through the months; Week shows one week, each day with its rows and their buttons. A day lists three rows, then says how many more, and a click shows its week. On a phone each day shows how many rows it holds, and a tap shows its week. Rows in other months are counted below it (“Not shown: 2 rows earlier”), and rows with no date in due are listed under No date. by= must name a field of type date, or the save refuses it and names the date fields. When the page has a row’s own view, a row’s first field opens it. With scripts off, the calendar is a list of the rows by day, earliest first.
for=rowOnly the rows that refer to the row a reader opens, on a collection with a row field. See Comments on a row.
where=status:openOnly the records whose field equals the value. To name more than one field, write a list: where=[status:open, priority:high], and a record must match each one. The value is read as the field’s declared type, so where=seats:2 matches the number 2, and where=done:true matches a boolean. Equality only. where=@mine shows each reader only the rows they added; with conditions, write where=[@mine, status:open].
sort=due_dateThe order of the rows, by one field: lowest first, or sort=due_date:desc for highest first. Numbers sort by size, and text and dates sort as text. A record without the field comes last. The block orders at most 500 records. When more match, it orders the 500 whose record ids sort last as text (a record’s id is its key’s value), and the others are not in the order, even one that would come first. The table then says “Sorted among 500 of the matching rows. Other matching rows are not in this order.” Without sort=, the newest key comes first. sort=@created_at:desc puts the rows added last first, and sort=@changed_at:desc the rows changed last.
edit=[title, due]An Edit button on each row the reader may change, with a form of these fields filled with the row’s values, and a pencil beside each of these values to change it in place. The key and a generated field cannot be edited. See Let readers change and remove rows.
actions=[Done: status=done]A button on each row the reader may change, that sets one field to one value in one press: here Done sets status to done. List several: actions=[Done: status=done, Reopen: status=open]. A row whose field already holds a button’s value does not show that button, so a done task offers no Done. The value must be of the field’s type, and one of the field’s fixed values when it has them. A button can also run a loop on the row instead: actions=[Draft a reply: run draft_reply]. See Buttons that run a loop.
filter=[status, title]Fields the reader may filter by. A field with fixed values shows each of them as a button, with All first, even a value no record holds yet. Another field with eight or fewer values among the block’s records shows them as buttons the same way; any other field shows a search box, which keeps the records whose field contains the words, ignoring case. A filter only narrows what the block already shows: it never shows a record that where= or the reader’s own-rows rule leaves out.

A table cell shows at most 160 characters of a value. Every value is shown as text, never as markup. The one exception is a file a reader uploaded: an image shows as a small picture and any other file as a link (see Let readers upload a file).

A reader can also sort a block and see more of it. A table’s column header sorts by that column: lowest first, then highest first, then back to the block’s own sort=. A list or cards show Sort by with each field. When a block has more records than its limit=, Show more adds the next ones below. These read the block’s records again, and the order and the next rows cover at most the block’s first 500 records, as sort= does. On the page’s own address with scripts turned off, the buttons, the sort and the next rows are links (Next rows and First rows move between pages of the block), and a search box says it needs JavaScript. Each of those links can be shared: opened later, with scripts on or off, it shows the same sort, filter and rows.

The second step checks each block against your account. A block that names a collection your account does not have, a {{form}} included, is refused with the collections you have: make the collection on the Collections screen, or save the draft of a loop that declares it, first. A field the collection does not declare, in fields=, where=, sort= or a form’s fields=, is refused with the fields it has. A where= value that is not its field’s type is refused, with an example of one that is. A {{form}} block that lists no fields is refused with the fields a person can fill in. A collection that only a pipeline step writes declares no fields, so its field names are not checked.

A where= value is matched as written. where=assignee:me shows the records whose assignee is the text “me”, not the person viewing the page. The save warns about a value like me, self or @user, names where=@mine as the way to show each reader their own rows, and the publish card shows each value in quotes.

Who added a row

pio remembers who added each row: the person whose page form or loop conversation saved it. A row a pipeline or a seed added has nobody, and so does a row sent through a page’s form by a visitor the page does not know.

  • where=@mine keeps a block to the rows the reader added. It matches by who added the row, never by a field, so a field that names a person does not change it, and a reader’s filter can only narrow it. A reader the page does not know, such as an anonymous visitor, sees no rows and the line “This shows each person only the rows they added, and this page does not know who you are.”
  • @created_by shows You on the reader’s own rows. On another member’s row it shows the name that member gave, and Someone when they gave none, or for anyone outside your team. It never shows an email address. A row nobody added shows a dash, ”—”.
  • @created_at shows the day, in UTC, like “19 Oct 2026”. A reader can sort by it, not by @created_by.
  • @changed_at shows the day the row was last changed, in the same way, and a reader can sort by it too.
  • @changed_by shows who changed the row last, in the words of @created_by: You, a member’s name, or Someone, and never an email address. A change through the page or by a loop working for a person names that person. A row a pipeline or a seed wrote last shows a dash, ”—”. A reader cannot sort by it.
  • edit=, actions=, filter= and a where= field cannot name any of them: pio fills them in, so the save refuses them, and a form never asks for them.

A row’s own view

Add {{record tasks from=row fields=[title, status, due]}} to a page that has a {{collection tasks}} block. Each row’s first value in that block becomes a link, and the link shows that row in the from=row block, at the same address, with a Close link. The address carries the row, so the link can be shared, and it works with scripts off. Until a reader opens a row, the block shows nothing.

  • It shows only a row the reader may read: the page’s read, the collection’s rows: and each reader’s own rows hold, as for every block. A row they may not read, or one that was removed, shows “That row is not on this page any more.”
  • It takes fields=, and edit= and actions= as a table does: Edit, the buttons and Delete show on the row for a reader the page lets change or remove it.
  • A page shows a collection’s opened row in one place, and a from=row block needs a {{collection}} block of the same collection. The save refuses a page that breaks either rule.
  • The Tasks starter opens a task this way, above its board.

Comments on a row

A collection whose row field refers to the rows of another (see A field that refers to a row) shows under the row a reader opens. For comments on a task, make a task_comments collection with a field task of type row, a row of tasks, and a text field. Then add, below {{record tasks from=row}}:

{{collection task_comments for=row fields=[text, @created_by, @created_at] view=list sort=@created_at}}
{{form task_comments fields=[text] for=row}}
  • for=row shows only the comments on the row the reader opened. Until they open one, both blocks show nothing. With view=list, each comment leads its line, and Added by says “You”, the commenter’s name or “Someone”.
  • The form saves each comment under that row. The reader never types the row: pio fills it in.
  • A reader sees a comment only if the page lets them read the comments and the row it is on. A row they may not read shows no comments, and its form saves nothing.
  • A collection with a row field shows on a page only so: any other block over it, without for=row, is refused at the save and names for=row. To read every comment, open the collection on Collections.
  • When a row is removed, its comments stay in the collection and show on no page, because no reader can open that row any more.

Write [Orders](page:orders), with the other page’s page_id. Each reader gets the address that serves them: in the console, the page’s console address; on the public address, the other page’s public address. Where the reader cannot open that page (it is not published, its readers do not name them, or on the public address it has no public address), the words show without a link. A page on your site through the embed shows the words without a link. The save refuses a page_id your account has no page of, and names the pages it has.

Preview as a reader

The preview shows the page as one reader sees it. Read as offers the page’s own readers, for example an anonymous visitor on a page for visitors, and opens on the widest of them: anonymous visitors, then verified users, members and admins. A block whose collection does not grant that reader shows why in its place, and names the fix: add the reader to that collection’s read. The rest of the page still shows.

A block that cannot show for any other reason also says why in its place: a record id the collection does not hold, or a collection no loop writes any more.

A form only some of the page’s readers may use is a choice, not a mistake. When the collection’s write names another reader of the page, the preview says so in its place, for example “only admin may use this form, so member readers see no form here”, and names no fix. The People and Questions and answers starters are this case: their forms are for admins. When write names none of the page’s readers, the preview names the fix: add the reader to that collection’s write.

A block that has nothing to show yet, such as a table whose collection has no records or a scratchpad nothing has written in, takes no space on the published page. The preview still draws it, so a page that is mostly empty for now does not look unrendered: an empty table shows its column headings and “Collection tasks: no records yet. Readers see nothing here until the first.”, and a scratchpad says it has nothing written yet. A {{loop}} block shows as a frame of the chat’s size with the loop’s name, because the chat runs only on your site. The card that asks you to approve a page a loop proposes shows the page the same way.

Publish

Press Publish…. The publish card, “Publish Tasks?”, says who can open the page and who can change its rows, in one or two sentences, before anything is published. Under Show the full definition it shows what the server read from your draft:

  • who may read the page,
  • each collection it may read, who reads it, who writes it, and who may change or remove its rows from the page (any row, or only the rows they added),
  • one sentence for each block: what it shows, and for whom, and what its rows let a reader do: its Edit form’s fields, each button and the change it makes, and Delete, for example “Member and admin may change any row here: Edit (title, due); Done sets status to “done”.”,
  • what it warns about but does not refuse: an embedded loop with nothing published, whose chat shows nothing yet; one that cannot change the page because its capabilities omit client_navigate; and a loop listed under loops that no {{loop}} block embeds, which the page may open but never shows. Add the block, or remove the loop from loops.
  • a grant or an address the page’s readers cannot use: a collection that lets readers the page does not have add, change or remove rows, a public address on a page whose readers include neither anonymous visitors nor invited readers, or embed on a page whose readers include neither anonymous visitors nor your signed-in users. Each warning names the fix: remove the grant or the address, or add the reader.

The fold also shows the document itself. When the draft changes a published page, the card asks “Publish the change to Tasks?” and leads with what changes against what people see now, as a change from LoopAssistant does; the rest is under Show everything else. Press Publish Tasks, or Publish the change, to publish. Publishing approves the document as it stands, and the published record is what readers see. A published version is never written again.

If the draft is saved again while the card is open, the publish is refused: review it again.

Open a published page

Once a version is published, the page’s screen shows Open in console. It opens the page in a new tab at its console address, app.plutonium.io/p/<page id>: the published version, live, as you, never the draft. The page fills the screen as its readers use it, under one Edit page link back to its screen on Pages. Its loops run and its forms save, as they do on your site, so an internal page is usable here. It opens as you when the page names your reader: a member as a member, the account’s owner as an admin. A page whose readers do not include you says so and links to its preview. A chat you start there is yours: it counts toward the loop’s per-person month for you, as for any person, and the loop’s month and the wallet pay.

Preview as its readers opens app.plutonium.io/pages/<page id>/preview: the same live page, with View as in the bar above it. Choose yourself, or An anonymous visitor when the page takes them: the page as a new visitor meets it, with its loops running and its forms saving. You are a new anonymous visitor each time you choose it, and a chat counts as your preview, not as a stranger’s. A page whose readers are only signed-in users of your site cannot be opened as one there; the editor’s preview, with Read as, shows any reader, and it is also the only place a draft is previewed.

Only members and admins of the page’s own account can open either address. Signed out, it asks you to sign in and then shows the page. A page id your account does not hold reads “That page does not exist.”, and a page not yet published says so and links back to its editor.

When the page can be read at its public address (see below), the screen also shows Open public page and Copy public address. When it cannot, the screen says in one line what the published version lacks.

Pages on Home

Home lists your published pages, so the ones you use most are a click away. Each row says who may read the page, with Open and Copy link, which copies its public address when it has one. Open goes to the address that serves you: the console address when the page’s readers include you, else its public address in a new tab. A page that neither address serves has no Open, and its row says what the page lacks.

With no page pinned, Home lists the five newest published pages. To choose them yourself, open a published page on Pages and press Pin to Home: Home then lists only the pinned pages, in the order you pinned them, at the top of the screen. Unpin from Home takes one off; when the last one goes, Home lists the newest again. A pin stays when you publish a new version, and goes when you delete the page. A page that was never published cannot be pinned, because Home would have nothing to open.

Put a page on your site

A page reaches readers outside the console when its document says who they are and where the page may be framed:

access:
  readers: [anonymous]
  identity: {kind: anonymous, limits: {per_ip: 30/1m}}
delivery:
  kinds: [embed]
  origins: [https://shop.example]

Publish it. The page’s screen then shows Put this page on your site: an <iframe> to paste into a page of a site under origins, and Copy. Accent colour sets the frame’s buttons and links for that snippet, checked as for a chat (your own accent colour). On any other site the frame says it is not set up for that site, and the browser’s console names the site it was framed in; add the site and publish again. If the page declares embed with no origins, the screen says so instead of giving code that cannot work.

Sites the page can be pasted on lists the page’s origins on its screen. Type an address under Add a site and press Add and review, or press Remove… beside one: the change is saved and the publish card opens, and nothing changes on your site until you publish. A site must start with https://; a path is cut to the site itself, because the frame is allowed by site, not by page. https://www.yourshop.com and https://yourshop.com are two sites.

If the page’s readers are your own signed-in users (readers: [user] with an identity of kind signed_assertion or oidc), the frame asks your page who the reader is. The screen shows the few lines your page adds to answer it: a fresh assertion signed with the page’s secret, or the reader’s id token from your sign-in provider, each time it asks. The page’s signing secret is on the same screen, for an owner.

While the frame waits for your page’s answer, the reader sees “Signing you in…”. If your page never answers (those lines are missing, or the page is for members and admins only), the frame adds after ten seconds that the page is for signed-in readers and the site has not signed them in. It keeps waiting, so a late answer still opens the page. The browser’s console carries the page door’s own sentence, for you.

Give a page its own address

A page can also have an address of its own on plutonium.io, for anybody who has the link. On the page’s screen, under Who can open it, choose Anyone with the link, without signing in. The choice saves the change and opens the publish card, which says who can open the page and who can change it; nothing changes for readers until you publish. The change adds anonymous to the page’s readers, sets public_path: true under delivery, and, when the page names no identity, signs visitors in as anonymous readers with a limit of 30 requests a minute from one network. It changes only who may read the page: no collection’s write, change or remove list changes, so a visitor can add to or change rows only where the page already let anonymous readers do so. Nor does it change who reads a collection’s rows: where a collection’s read does not name anonymous, visitors see that collection’s blocks empty, and the publish card says so, for example “Anyone with the link sees none of the rows of tasks: only members and admins read them.” Under Rows they can see, the card offers one choice for each collection a block on the page shows, unticked: “Let anyone with the link see the rows of tasks: title, status, owner and due. Off. Ticked, they see 12 rows today, and every row added later.” It names the fields visitors would see, never a sealed one. A collection the page only takes rows into, such as a request form’s, is not offered: nothing on the page would show its rows. Ticking it adds anonymous to that collection’s read in the page’s text and saves the change; the card then reads it again. Its write, change and remove lists do not change. Members of your account, in the console takes the page back to its members. The choice is offered for a page read by members, admins or anonymous visitors. A page for the people you invite, or for your site’s signed-in users, says who reads it in its text, and its screen names what to change instead.

The same change, written by hand, reads:

access:
  readers: [anonymous]
  identity: {kind: anonymous, limits: {per_ip: 30/1m}}
delivery:
  kinds: [embed]
  origins: [https://shop.example]
  public_path: true

Publish it. The page is then at https://plutonium.io/p/<public id>, where the public id is the pg_… id minted at the page’s first publish; Open public page on the page’s screen opens that address, and Copy public address copies it. A visitor gets the published version as an ordinary web page, and each table shows only what the page’s collections let an anonymous reader see. A thin bar above it names your account and the page’s title. A {{loop}} block’s chat runs there as it does in the frame on your site: the visitor is the page’s anonymous reader, signed in once for the page and its chats, and the page’s per_ip limit counts them. The address is the page’s own, so it needs no entry in origins, and neither does any loop the page embeds. With scripts turned off, the page still reads and its forms still save, and each chat says “This chat needs JavaScript. Turn it on and reload the page to use it.” If the chats cannot start, the page stays as it is and says so above it.

The address shows nothing that is not public. If the page does not set public_path: true, or its readers include neither anonymous nor invited, a visitor reads “This page is not public.”, and the page’s screen shows no public link but “It has no public address. To give it one, choose Anyone with the link under Who can open it, then publish. An address that names no page reads “There is no page at this address.” A visitor’s network that asks too often reads “Too many requests from your network. Try again in a minute.”

Open a page to the people you invite

A page with its own address can be for named people only, such as a client and their team. They sign in at the page’s address with a code that plutonium.io emails them, or with Google. They need no account, no password and no member seat, and the page sees each of them as their own reader. Name invited among the page’s readers, and say who it invites:

access:
  readers: [invited, member]
  invited: {emails: [dana@client.co], domains: [acme.co]}
delivery:
  public_path: true
collections:
  - {name: requests, read: [invited, member], write: [invited], rows: owner_field:requested_by}

emails names whole addresses. domains names a domain, such as acme.co, and admits any address at that domain that the person proves is theirs by entering the code sent to it, or by signing in with Google with an address Google has verified. A domain is exact: acme.co does not admit sub.acme.co. An invited page needs public_path: true, because its readers sign in at its address. It needs no identity block. The page’s screen shows who it invites under Who it invites.

At the page’s address, a person who is not signed in reads “Sign in to open” and the page’s title, and that the page “is for the people it invites”. They type their email address and press Email me a code. The page then says “If that address is invited to” the page, “a 6-digit code is on its way to it.” It says the same words for every address, so nobody can find out who is invited by trying addresses, and only an invited address gets an email. The code works for 10 minutes. An address can get 2 codes in 10 minutes, and a code takes 5 tries. They type the code and press Open the page. They then read the page, and a bar above it says “Signed in as” and their address, with Sign out. The sign-in lasts 12 hours, for that page only. When it ends, or after they sign out in another tab, the page’s chat says “Your sign-in to this page has ended.” and asks them to reload the page and sign in again.

Instead of a code, they can press sign in with Google and choose the Google account for the invited address. They come back to the page signed in, as the same reader a code signs in. If the page does not invite that address, they read “That address is not invited to” the page. If Google has not verified the address, the page says so, and they can verify it in their Google account or use a code. Signing in with Google does not sign them in to plutonium.io and does not make them a member of your account.

If you take an address out of invited and publish, that person is refused at once, though they signed in before; if their code was already on its way, entering it reads “That address is not invited to” the page. When the page’s readers name member, a member of your account presses Sign in as a member and reads the page as themselves, as in the console.

With rows: owner_field:<field>, each invited reader sees only their own rows, and a row they save through a {{form}} holds their own identity in <field>. The identity is an id that plutonium.io gives the reader, never their address.

Each code is an email your account sends. It costs what an email a loop sends costs, and it counts against your account’s daily email quota, which your loops’ emails share (1,000 emails a day unless an operator changes it). When the day’s quota is used up, no more codes are sent that day, and the page still says a code is on its way. Unlike a loop’s email, a code is not held from the wallet before it is sent, so a person can sign in when the wallet is empty: the codes a page sent in an hour are charged together soon after the hour ends. The quota is what bounds that, at most 1,000 codes a day.

Let readers add a record

A {{form}} block lets a reader add a record to a collection, such as a visitor joining a waitlist. Name the reader in the collection’s write, and list the fields the form asks for:

access:
  readers: [anonymous]
  identity: {kind: anonymous, limits: {per_ip: 30/1m, monthly_writes: 1000}}
delivery:
  kinds: [embed]
  origins: [https://shop.example]
collections:
  - {name: waitlist, read: [anonymous], write: [anonymous]}
template: |
  {{form waitlist fields=[email, company]}}

  People waiting: {{count waitlist}}
  • monthly_writes is the most records anonymous visitors may add through the page’s forms in one calendar month (UTC). A page whose write names anonymous must set it, or its save is refused with that fix. Past it, a visitor reads that the page has taken all the saves its owner allows this month, and when it takes more.
  • A field with fixed values is a list to choose from, and so is that field in a row’s Edit. A value outside the list is refused, naming the list.
  • Each value is checked against the field’s type, and a wrong one is named at its field. The form must include the collection’s key field, which names the record. If the key is generated, the form leaves it out, and each save makes a new record under a new id.
  • A form only adds. If a record with the same key is saved already, nothing changes. A reader who may read the collection is told the record exists; anyone else sees the ordinary “saved”, so a form never tells a visitor what somebody else saved.
  • If rows: owner_field:<field> keeps the collection to each reader’s own records, the form fills <field> with the reader’s identity, so the record is theirs.
  • If you publish the page again while a reader has it open, their save is refused and asks them to reload the page.

A form saves in the frame on your site and on the page’s own address (/p/<public id>). On the page’s own address it works with scripts turned off: a save reloads the page, which says above the form whether the record was saved or what to fix. It saves only when sent from that page, never from another site.

Email someone when a form saves

Add email= to a form, and each save sends an email:

template: |
  {{form waitlist fields=[email, company] email=owners}}
  • email=owners emails each owner of the account, as the account’s owners are when the record is saved.
  • email=<address> emails one address: email=sales@example.com. A member’s address receives at once. Any other address gets one confirmation email from plutonium.io when you publish the page, and receives nothing until that person confirms. The link is valid for 7 days.

After you publish, the page says what the publish did about each address. Afterwards, under Who its forms email on the page in Pages, each address that is no member’s shows where it stands: confirmed, asked, lapsed, never asked, unsubscribed, or bounced. If no confirmation is waiting, press Send a confirmation email there. While an address waits on a confirmation, Home names it under Needs you. The link in the email opens the saved row in Collections.

The email names the page, the form and the collection, and links the row in the console; it never shows what was typed. Open the row to read it. One email goes to each recipient for each record a save adds. A save that is refused, or that finds the record saved already, sends nothing. When you publish, the page’s review says who each form emails.

Each email is charged like a loop’s email, and every email carries an unsubscribe link. A person who unsubscribes gets no more email from your account. An email to an address that has not confirmed, or that is no longer a member, is held and not sent, and it is not sent later either.

Let readers upload a file

Declare a file field on the collection, and list it on a form, or in edit= so a reader can replace it:

template: |
  {{form claims fields=[title, photo]}}
  {{collection claims fields=[title, photo] edit=[photo] where=@mine}}
  • A reader picks a file, the page uploads it at once and says “Ready to save: dent.png”, and Save stores it with the row. A form whose save is refused keeps nothing.
  • A page takes png, jpg, gif, webp, pdf, txt, csv, md, docx and xlsx files, of at most 10 MB each, and at most 1 GB of files a month across its readers. It never takes svg or html. The page says which, and why, before the file is sent. A megabyte here is 1,048,576 bytes, as Windows counts it, so a Mac shows the largest file a page takes as 10.5 MB.
  • On a row, an image shows as a small picture and any other file as a link that downloads it.
  • A file is shown only to a reader who may read its row. The page makes each file’s link when it shows the row to that reader, and the link works for about ten minutes; anyone holding it can use it until then. A link can also end sooner. A picture that no longer loads shows its name: reload the page.
  • Replacing a file with Edit, or deleting its row, deletes the file. So does deleting the collection.
  • Uploading a file needs JavaScript. With scripts off the field says so, and the rest of the form still saves.
  • A loop’s run sees a file’s name, type and size, never what is in it.

Let readers change and remove rows

A page lets its readers change and remove rows only where the collection allows it (its access, see Who may change or remove rows from a page), and only as far as the page’s own entry narrows it:

collections:
  - {name: tasks, read: [member], write: [member],
     change: {by: [member], rows: own}, remove: {by: [member], rows: own}}
template: |
  {{collection tasks fields=[title, status] edit=[title] actions=[Done: status=done]}}
  {{form tasks fields=[title]}}
  • rows: own is the rows the reader added. A person who adds a task through the form may then edit it, press Done on it and delete it. Another reader sees the task and has none of those buttons on it, and a change sent for it anyway is refused.
  • For a page your invited readers sign in to, name invited in the collection’s access first, then in the page’s entry: change: {by: [invited], rows: own}. Each invited reader then changes only the rows they added.
  • Edit opens a form under the row, filled with its values, and Save writes them. Delete shows on each row the reader may remove and opens “Delete this row? It cannot be brought back.” with a second Delete, which removes it. Both work in table, list and cards.
  • Each value of an edit= field also changes where it stands: a small pencil beside it opens an input in its place (a list of choices for a choice field, a box for a yes-or-no field), Save writes that one field and Cancel or Escape leaves it as it was. It is the same change as Edit, checked the same way, and shows only on the rows the reader may change. If the change is refused, the reason shows under the input. It works in table, list, cards and a board, where scripts run; with scripts off, Edit is the way.
  • The server decides each button for each reader and checks every press again, so a button never works for somebody it was not shown to. If the collection’s access is narrowed after the page is published, the page’s buttons narrow with it at once.
  • edit= or actions= on a collection the page lets nobody change is refused at the save, and so is a field the reader may not set.
  • On the page’s own address the buttons work with scripts turned off.
  • The block says what happened above its rows: “Saved.” or “Removed.”, or why not: “Nothing was changed: that row is somebody else’s.” when a row was not the reader’s to change, and “Nothing was changed: that row is no longer there.” when somebody removed it first.

Buttons that run a loop

A row button can start a run of one of your loops on that row, rather than set a field:

collections:
  - {name: replies, read: [member, admin], change: {by: [member, admin], rows: any}}
template: |
  {{collection replies fields=[question, draft] actions=[Draft a reply: run draft_reply]}}

Page rights name admin apart from member: an admin is not a member here. To press the button yourself as the account’s admin, name admin in the entry’s read and change, as above, and in the collection’s own access on Collections; with member alone you see the rows and no button.

  • The loop declares that it runs from these buttons, with a page trigger on the same collection: {kind: event, source: page, collection: replies, concurrency: 2}. See From a page button. The save refuses a button whose loop’s published version has no such trigger, and names the trigger to add. A loop with no published version is a warning: its button starts nothing until you publish it.
  • The button shows where Done would: on each row the reader may change, and the server checks the press again. It is never hidden by the row’s values.
  • The reader may press it only where the collection’s own rights let them change the row. Set those on Collections, with Who may change rows of …: a loop’s own collection takes them there too, as the loop owns only its fields.
  • A loop whose only trigger without a person is this page trigger needs no outputs: its run’s result is the changed row.
  • Do not add the loop to the page’s loops. That list is the chats the page opens for its readers. A button’s loop runs with no user, and the button names it.
  • Each press starts one run, about that row. The run reads the row as untrusted data, and it may change that row and no other, whatever else the loop may write. The publish card says so: “Draft a reply runs draft_reply on the row, which may change that row and no other.”
  • The press writes nothing itself. The block says “Draft a reply started. This row changes when its run finishes: reload the page to see it.” The run shows in Runs, titled Button pressed.
  • The loop’s own caps bound the presses: its trigger’s concurrency, its per-subject cap (one row is one subject) and its monthly budget. A press over a cap starts nothing, says “Nothing was started”, and asks the reader to try again later. An anonymous reader’s press counts toward the page’s monthly_writes, as any button does.

Change a published page

On the page’s screen, type what you want to change under What do you want to change?, such as “Add a due-date column”, and press Ask LoopAssistant. LoopAssistant opens in the Assistant panel, in a tab named for the change, with your words already sent, and proposes the change for your approval there. While a change waits to publish, or on a page that was never published, what you ask adds to that change, and the box says so; Publish the change… and Discard the change… stay beside it.

To change the page yourself, press Change it yourself. Your edit is saved as the page’s waiting change, and readers keep the published version until you press Publish the change…. While a change waits, Where it stands reads “published · a change waiting”, and the page says “A change is saved and not published. People still see what is published.”

To throw a waiting change away, press Discard the change…. Readers keep the published version. A page that was never published has nothing else, so discarding its only draft removes the page.

How a page is used

Metrics, under Pages, shows each published page’s visits, the readers it turned away, the rows readers added, changed and removed, the runs its row buttons started, and the page actions it answered for a loop’s chat, over the last 7 or 30 days. Only readers from outside your account count. See Metrics.

Who may do what

OwnerMember
Write, preview and save a draftYesYes
Publish a draftYesYes
Discard a draftYesYes
Pin a page to Home, or unpin itYesNo
Delete a pageYesNo

Every member sees the same pages on Home: an owner chooses them for the account. Delete page… removes every version of the page, and the screen goes back to the list. Only an owner sees it; a member sees that it is the owner’s.