Browse docs

Start here

examplesGetting started with Flowdocumentation

Design

FoundationsLanguage architecturePhilosophy

Language specification

Program checkingConcurrencyDataEffectsResults, Tool problems, and faultsGrammarHistoryModules and importsLanguage specificationEvaluationStandard libraryToolsTypes

Runtime

Runtime architectureThe host boundaryDiagnosticsRunning a programThe history format

Guides

Writing programs that reach checkpointsImplementing Tools with a toolkitLoops that never returnRecursive delegationSharing types between Tool modulesHarnesses over tool registries

Sharing types between Tool modules

This guide is non-normative. It shows practice; it creates no semantics and states no guarantee. Modules owns imports, versions and cycles, types owns pub and opaque, tools owns which types decoding may build, and the runtime ABI owns the decoding table.

Keep modules that share types in one repository

Tool modules often share types. A catalog returns a Document, a search Tool returns lists of them, and an editor takes one back:

// github.com/acme/docs/document.flow
pub type Document { pub id: Text, pub title: Text, pub body: Text }

// github.com/acme/docs/catalog.flow
use "./document"
pub type CatalogError { NotFound(Text), Unavailable }
@tool pub flow read(id: Text) -> Result<document.Document, CatalogError>

// github.com/acme/docs/search.flow
use "./document"
pub type SearchError { BadQuery(Text), Unavailable }
@tool pub flow find(query: Text) -> Result<List<document.Document>, SearchError>
Open in playground →

Put such modules in one repository and import each other with relative paths. Within a repository, modules may import each other in cycles, and they are released together under one version (modules).

Splitting them across repositories brings two problems:

  • Cycles across repositories are refused. If catalog lives in one repository and search in another, and each needs a type from the other, no version of either could be released first. The program is refused.
  • A type is tied to its repository's version. A program uses one version of each repository (modules). Types shared across repositories make every consumer resolve both at compatible versions, and a major release of the one holding the types forces a release of the other.

When types must be shared across repositories, put them in a repository of their own that imports nothing from its users, and let every Tool repository import it. The dependency then runs one way.

Host types follow the same rule. A @tool type is nominal and belongs to the module that declares it (tools), so the Tools that make a Page and the Tools that take one back should live where they can all name that one declaration.

pub and opaque for types that cross

A type that crosses a Tool boundary is data, and a type a reply builds must be one decoding can build: no opaque part anywhere inside it (tools). Visibility otherwise works as usual:

  • Mark fields pub when other modules read them. A record with a private field can still be decoded and sent to a Tool; private fields hide how a value is built, not what crosses. Outside its module such a record can be read through its public fields but not built raw or rebuilt with ..base.
  • A pub enum exposes every constructor. Reply enums are usually pub, since callers match on them.
  • Use opaque for values minted after a check, and only for those. An opaque type cannot come from any decoder, its own module's included, so a Tool reply or json.decode can never forge one.
// github.com/acme/review/review.flow
pub type Plan { pub steps: List<Text>, pub owner: Text }
pub opaque type Approved { Approved(Plan) }

pub flow approve(plan: Plan, verdict: Verdict) -> Option<Approved> = match verdict {
    Accepted => Some(Approved(plan)),
    Rejected(_) => None,
}
Open in playground →

A model or a person replies with a Plan or a Verdict, which decode. Only approve makes an Approved, and a function that takes one knows the check happened in this run. A checkpoint holding an Approved is rebuilt on resume as the program made it; that is not decoding (history).

Do not put an opaque type inside a reply type. The declaration is refused, because the reply could never decode. Return the plain value and convert it through the owning module's function.

Decoding rules in practice

Decoding is strict, directed by the declared type, and never repairs a value (abi). Some habits follow:

  • Make a field Option if a provider may omit it. A missing field fails decoding unless its type is Option, and null is accepted only there.
  • Extra fields in a reply are ignored. A provider can add fields without breaking older programs. A run's input is stricter: an undeclared field refuses the run.
  • New constructors are not ignored. An unknown constructor name is refused, so a reply using a constructor an older program does not declare comes back as BadReply. Add constructors to a reply enum as a deliberate, version-raising change, and handle BadReply where it matters.
  • Numbers are read exactly. An Int field needs a whole value, so 3.5 fails to decode as one. Use Decimal for amounts that may carry fractions.
  • Nothing is coerced. The text "42" is not an Int, and no default is filled in. Ask the provider for the declared form rather than widening the type.
  • A decoded value is only well-formed. Decoding proves Flow can interpret the value, not that it is true. Check what matters before acting on it, and record the check in an opaque type if later code must rely on it.

When the shape is genuinely unknown, decode into json.Value or keep the text, and decode the parts you need later with json.decode at a declared type (stdlib).