Common patterns

Common patterns #

Reach for the shape that matches the brief. Each is an idiom, not a whole file — the surrounding imports, constructions, and listen line are the anatomy chapter's job, and every type and field name here is illustrative.

inbound-intake #

Something arrives, a record is created or updated, the team is told.

person = write crm-[:People]-> {
  unique by (Email)
  Email: msg.From
  Name:  coalesce(ai("the sender's name, cleaned — no email address, no 'via', no company suffix: ${msg.From}"), msg.From)
}

intake = only(chat-[ch:Channels WHERE ch.Name == "intake"]->)
if (intake == null) { fail("no #intake channel") }
write intake-[:Messages]-> { Message: "New sender: ${msg.From} (${person.externalId})" }

Key the identity on something actually carried in the message, so a second arrival updates rather than duplicates. The ai() touch is a micro-judgement — tidying a display name; pulling an entity out of the prose would be an extract job — and it is wrapped in coalesce because Name is required and a judgement can come back with nothing.

multi-target #

Several systems hearing one event is several writes in one body; a later write quotes an earlier handle.

record = write crm-[:Companies]-> { … }

ops = only(chat-[ch:Channels WHERE ch.Name == "ops"]->)
if (ops == null) { fail("no #ops channel") }
write ops-[:Messages]-> { Message: "Logged to the CRM: ${record.externalId}" }

For writes that genuinely don't depend on each other, pass them to all as functions, and they run at once:

all(
  () => { write crm-[:Companies]-> { unique by (FUZZY Name), Name: msg.Subject } },
  () => { write sheet-[:Rows]->    { unique by (Company),    Company: msg.Subject } },
)

Each one is a function — () => { … } written where it stands, or the name of one declared elsewhere — and every one runs at once (all(f, g, { concurrency: 1 }) runs one at a time). all finishes when every function has; one that waits parks the run, so there is no await. A function reads anything bound before all and never another's bindings. Work that has to come back out is returned: the answer has one slot per function in the order written, and r[0] reads what the first returned. Functions that only act need no binding at all, as here.

compose-a-report #

Many small things into one readable artifact: a block returns a line each, Text.join assembles them, pdf.render turns the text into a PDF file for a file-typed field.

lines = msg-[f:Attachments]-> {
  return "- ${f.Name}"
}

digest = "Attachments on ${msg.Subject}:
${Text.join(lines, "\n")}"

write record-[:Files]-> { File: pdf.render(digest) }

pdf.render draws the string exactly as composed, so the layout you write is the layout in the file. Anything a block can iterate — extracted entities included — can feed one.

scheduled-digest #

A schedule fires, a query gathers, a message goes out. The tick carries nothing you need — the query is the input.

function `Weekly Digest`(t: <cron-[:Ticks]->>) {
  lines = crm-[c:Companies WHERE c.`Created At` WITHIN 7d ORDER BY c.`Created At` DESC LIMIT 20]-> {
    return "- ${c.Name}"
  }

  ops = only(chat-[ch:Channels WHERE ch.Name == "ops"]->)
  if (ops == null) { fail("no #ops channel") }
  write ops-[:Messages]-> { Message: "New this week:\n${Text.join(lines, "\n")}" }
}

listen as "Weekly digest" to cron-[k:Ticks WHERE k.Schedule == "0 9 * * 1"]-> fire `Weekly Digest`

sections-from-a-type #

A report whose sections come from a type rather than from a list written beside it: group the findings by the value, walk the type's members in the order they were declared, and look each group up.

type Thesis = <"Consumer" | "Infra" | "Health">

node Finding: "each company mentioned" {
  headline: <text> "one line about it"
  thesis:   <Thesis> "which thesis it fits"
}

function `Thesis Recap`(go: <manual-[:Invocation]->>) {
  found    = extract([go.Text], Finding)
  rows     = map(found, (f) => { return { thesis: coalesce(f.thesis, "Consumer"), line: f.headline } })
  by       = List.groupBy(rows, (r) => r.thesis)
  sections = map(members(<Thesis>), (th) => "${th}: ${count(coalesce(by[th], []))} found")

  ops = only(chat-[ch:Channels WHERE ch.Name == "ops"]->)
  if (ops == null) { fail("no #ops channel") }
  write ops-[:Messages]-> { Message: Text.join(sections, "\n") }
}

group-and-count #

A line per distinct value with its count (group by a key, count per group, distinct values): group with List.groupBy, then list the dict's keys in order.

authors = gh-[r:Repositories WHERE r.Name == "api"]->-[i:Issues WHERE i.State == "open"]-> { return i.Author }
by      = List.groupBy(authors, (a) => a)
lines   = map(sort(Dict.keys(by)), (a) => "${a}: ${count(by[a])}")

Grouping deeper than one level (people, then each person's repositories, then the issues in each) files each record under one record per key in the run's own graph, then walks the keys:

node Board {
  node people {
    name: <text>
    node repos {
      name: <text>
      node issues { title: <text> }
    }
  }
}

board = graph<Board> {}
gh-[r:Repositories]->-[i:Issues WHERE i.State == "open"]-> {
  p = write board-[:people]-> { unique by (name), name: i.Author }
  q = write p-[:repos]-> { unique by (name), name: r.Name }
  write q-[:issues]-> { title: i.Title }
}
lines = board-[p:people ORDER BY p.name]->-[q:repos ORDER BY q.name]-> { return "${p.name} in ${q.name}: ${count(q-[:issues]->)}" }

extract-and-connect #

Records pulled out of a message and written with their relationships intact: the people are declared inside the company, so each one lands attached to the right one.

import { vcUrlRetrieval } from plugins

node Company: "each company this message is about" {
  name:    <text> "the company's name"
  website: <text> "the company's official website"
  node person: "each person at this company named in the message" {
    name:      <text> "the person's full name"
    job_title: <text | null> "the person's job title, if given"
  }
}

pages     = vcUrlRetrieval(msg.Body)
content   = [msg.Body, ...map(pages, (p) => coalesce(p.text, ""))]
companies = extract(content, Company, { tier: "careful" })

map(companies, (c) => {
  company = write crm-[:Companies]-> { unique by (FUZZY Name), Name: c.name, Domains: c.website }
  c-[p:person]-> {
    write company-[:Team]-> { unique by (FUZZY Name), Name: p.name, `Job Title` ?: p.job_title }
  }
})

Two things here you cannot derive:

declare-once #

The same record extracted in several places, written the same way each time — declare it once, with its words, and reuse it:

node Entry: "each company named in this text" {
  Name:        <text> "the company's name"
  Description: <text> "what the company does, in one sentence"
  node person: "each person at the company" { Name: <text> "their full name" }
}

function `Intake`(go: <manual-[:Invocation]->>) {
  found = extract([go.Text], Entry, { tier: "careful" })
  map(found, (e: <Entry>) => {
    company = write crm-[:Companies]-> { unique by (FUZZY Name), ?...e }
    e-[p:person]-> { write company-[:Team]-> { unique by (FUZZY Name), ?...p } }
  })
}

human-reviewed-intake #

"Let me paste something in, but let me check it before it's saved."

found = extract([go.Text, ...go-[:Files]->.File], Company)

q = write asks-[:Check]-> {
  Prompt: "Save these companies and people?"
  Detail: "Review the extracted records before they're written."
}

r = race(
  () => {
    answer = await q-[:Response]->
    return answer.Answer
  },
  () => { await sleep(1d) },
)
if (r[0] == true) {
  map(found, (c) => { … })
}

Two things make it this shape rather than a thinner one: