@cosyte/pathways
Classes
DeliveryRejected
A delivery the receiver rejected for good: retrying the same message will not help.
instanceof DeliveryRejected holds for one made by any copy of Pathways in the process: the
pathways command bundles its own copy of this class, and a custom destination imports another
from the package entry, so the check goes by a shared mark rather than by class identity.
Example
if (ack.code === "AE") throw new DeliveryRejected("AE");
Extends
Error
Constructors
Constructor
new DeliveryRejected(
reason):DeliveryRejected
Parameters
reason
string
A short, stable reason, such as an ACK code. Never message content.
Returns
Overrides
Error.constructor
Properties
reason
readonlyreason:string
A short, stable reason, such as an ACK code. Never message content.
Methods
[hasInstance]()
static[hasInstance](value):boolean
Whether value is a DeliveryRejected from any copy of Pathways; a subclass checks as usual.
Parameters
value
unknown
Returns
boolean
EncryptionError
A value that could not be encrypted or decrypted. Its message is fixed text, with at most the master key's id, which is never secret: never the value, a key, or anything a message holds. A value that fails its integrity check always ends here, never as a garbled message.
Example
try {
await decrypt(stored, keys);
} catch (error) {
if (error instanceof EncryptionError) console.error(error.code, error.keyId);
}
Extends
Error
Constructors
Constructor
new EncryptionError(
code,keyId?):EncryptionError
Parameters
code
What went wrong.
keyId?
string
The master key's id, when the failure concerns one.
Returns
Overrides
Error.constructor
Properties
code
readonlycode:EncryptionErrorCode
keyId
readonlykeyId:string|undefined
The master key's id, when the failure concerns one.
LeaseLostError
A fenced write that changed nothing: the epoch it carried is no longer the engine lease's, so
another engine has taken the lease since, or the row it was to change moved under that engine.
The engine that sees it stops sending and taking at once and exits (log code lease-lost).
Example
if (error instanceof LeaseLostError) process.exitCode = 1;
Extends
Error
Constructors
Constructor
new LeaseLostError():
LeaseLostError
Returns
Overrides
Error.constructor
Properties
code
readonlycode:"ERR_PATHWAYS_LEASE_LOST"="ERR_PATHWAYS_LEASE_LOST"
PassFailed
Why sink.receive refused a message of a channel that stores nothing (storage.content: none),
where each message is transformed and delivered before receive resolves. permanent says
whether the sender should send it again:
false: a destination failed in a way that may pass (it was down, it timed out, the pass ran pastpassTimeout), so the sender should send again.retryAfterMsis how long it might wait. An MLLP source closes without an ACK and an HTTP source answers 503 withRetry-After.true: sending the same message again cannot help (the transform failed, or a destination rejected it). An MLLP source answersAE, an HTTP source 422, and a file source moves the file into itserrorDir.
A source that does not know this class treats it as any failed receive: it does not
acknowledge, and the sender sends again. instanceof PassFailed holds for one made by any copy
of Pathways in the process.
Example
try {
await sink.receive(body);
} catch (error) {
if (error instanceof PassFailed && error.permanent) answer("AE");
else closeWithoutAnswer();
}
Extends
Error
Constructors
Constructor
new PassFailed(
code,permanent,retryAfterMs?):PassFailed
Parameters
code
string
A short, stable code. Never message content.
permanent
boolean
Whether sending the same message again cannot help.
retryAfterMs?
number
For a failure that may pass, how long the sender might wait.
Returns
Overrides
Error.constructor
Properties
code
readonlycode:string
A short, stable code, such as PASS_TIMEOUT or DESTINATION_REJECTED. Never message content.
permanent
readonlypermanent:boolean
Whether sending the same message again cannot help.
retryAfterMs
readonlyretryAfterMs:number|undefined
For a failure that may pass: how long the sender might wait before it sends again.
Methods
[hasInstance]()
static[hasInstance](value):boolean
Whether value is a PassFailed from any copy of Pathways; a subclass checks as usual.
Parameters
value
unknown
Returns
boolean
SecretError
A secret could not be resolved. Its text names the secret, never a value. A provider can reject with one, so the code shows where Pathways reports the failure.
Example
throw new SecretError("ERR_PATHWAYS_SECRET_NOT_FOUND", "lab/api");
Extends
Error
Constructors
Constructor
new SecretError(
code,secret,detail?):SecretError
Parameters
code
What went wrong.
secret
string
The secret's name, never its value.
detail?
string
What went wrong, in words: never a value. Defaults to the code's own words.
Returns
Overrides
Error.constructor
Properties
code
readonlycode:SecretErrorCode
secret
readonlysecret:string
The secret's name.
Interfaces
AckRequest
What an AckBuilder is asked to acknowledge.
Example
const controlId = (request: AckRequest): string => request.header?.get("MSH.10") ?? "";
Properties
code
readonlycode:SourceAckCode
The code Pathways decided on.
header
readonlyheader:Hl7Message|undefined
The inbound message's MSH segment, parsed, or undefined when it has none that parses. Only
the header is parsed: never the segments after it.
messageId
readonlymessageId:number|undefined
The stored message's id, or undefined when it could not be stored. On a channel that stores
nothing, the message's pass id, which no command can look up.
problem
readonlyproblem:string|undefined
For an unreadable message, what was wrong, as a stable code: one of @cosyte/hl7's parse
codes (such as NO_MSH_SEGMENT), NO_CONTROL_ID, MULTIPLE_MESSAGES or
MLLP_TRAILING_BYTES.
reason
readonlyreason:AckReason
ArchivedDelivery
One delivery of an archived message, of any generation, as storage held it: every column of its row but its message's id and channel, which the record it sits in holds.
Example
const sent = (d: ArchivedDelivery): boolean => d.status === "delivered";
Properties
attempts
readonlyattempts:number
body
readonlybody:string|null
The output it carried, in base64; null once its message's content was dropped.
deliveredAt
readonlydeliveredAt:string|null
destination
readonlydestination:number
error
readonlyerror:string|null
Its last error text, which can quote the message.
failedAt
readonlyfailedAt:string|null
When it failed for good, when it did and the store recorded it.
generation
readonlygeneration:number
id
readonlyid:number
interruptedAttempt
readonlyinterruptedAttempt:number|null
The latest attempt found cut off, when one was.
nextAttemptAt
readonlynextAttemptAt:number
Epoch milliseconds before which it was not to be tried again; 0 when due at once.
sendingAt
readonlysendingAt:number|null
When an attempt in flight was handed to its destination, in epoch milliseconds.
sendingAttempt
readonlysendingAttempt:number|null
The number of the attempt in flight, beside sendingAt.
seq
readonlyseq:number
status
readonlystatus:DeliveryStatus
ArchivedMessage
One message in an archive: everything storage held about it when retention reached it, every
column of its row and of each delivery row, every generation. Bodies are base64, and null
(with no outputs) once its content was dropped. The index tags of its metadata are not kept:
they are derived from meta under the master keys.
Example
const body = (record: ArchivedMessage): Buffer | undefined =>
record.body === null ? undefined : Buffer.from(record.body, "base64");
Properties
body
readonlybody:string|null
The body as it arrived, in base64; null once dropped.
channel
readonlychannel:string
contentDroppedAt
readonlycontentDroppedAt:string|null
contentHash
readonlycontentHash:string|null
dedupKey
readonlydedupKey:string|null
The hash of who sent it and its control ID, for spotting a repeat.
deliveries
readonlydeliveries: readonlyArchivedDelivery[]
Every delivery, every generation, oldest first, each with its body.
duplicateOf
readonlyduplicateOf:number|null
error
readonlyerror:string|null
Why it failed, as stored: can quote the message.
generation
readonlygeneration:number
id
readonlyid:number
idempotencyKey
readonlyidempotencyKey:string|null
The hash that names it to its source, so it is stored once.
meta
readonlymeta:Readonly<Record<string,string|number|boolean>> |null
Its ctx.meta, which can hold identifiers; null when it had none or it was dropped.
outputs
readonlyoutputs: readonlystring[]
The outputs of its current generation, in order, in base64: those of its first destination.
receivedAt
readonlyreceivedAt:string
size
readonlysize:number|null
status
readonlystatus:MessageStatus
updatedAt
readonlyupdatedAt:string
ArchiveHeader
The first line of an archive: what it is, which channel's messages it holds, the master key it is
sealed under, when it was written and which retention step wrote it. mac is the trailer's HMAC
key, wrapped under that master key, in base64.
Example
const header: ArchiveHeader = { format: "pathways-archive", version: 1, channel: "adt", keyId: "dev", created: "2026-11-11T00:00:00.000Z", step: "metadata", mac: "..." };
Properties
channel
readonlychannel:string
created
readonlycreated:string
ISO 8601, UTC.
format
readonlyformat:"pathways-archive"
keyId
readonlykeyId:string
mac
readonlymac:string
step
readonlystep:RetentionStep
version
readonlyversion:1
AuditEntry
One entry of the audit log, as stored.
Example
const line = (entry: AuditEntry): string => `${entry.at} ${entry.actor} ${entry.action}`;
Properties
action
readonlyaction:"search"|"view"|"content-view"|"replay"|"resend"|"clear"|"retention"|"config"|"lease-release"|"migrate"|"channel-pause"|"channel-resume"|"channel-stop"|"channel-halt"|"channel-reload"|"verify"|"bulk-replay"|"archive"|"node-forget"
actor
readonlyactor:string
The operating system user who acted.
at
readonlyat:string
When, in ISO 8601 UTC.
channel
readonlychannel:string|null
detail
readonlydetail:AuditDetail
id
readonlyid:number
messageId
readonlymessageId:number|null
outcome
readonlyoutcome:AuditOutcome
AuditEvent
An entry of the audit log, once it is stored: who did what, to which message, and how it ended. The operator commands (and the engine's retention runs and configuration checks) write them; engine-wide hooks receive them, and channels' own hooks do not. Like every event it carries ids, counts, statuses, codes and hashes, never content, so a hook can forward the audit log to a file or a SIEM.
Example
if (event.type === "audit" && event.action === "content-view") {
siem.send({ at: event.at, user: event.actor, message: event.messageId });
}
Properties
action
readonlyaction:AuditEventAction
actor
readonlyactor:string
The operating system user who acted.
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC.
channel
readonlychannel:string|undefined
The channel acted on, or undefined for an entry about no one channel.
detail
readonlydetail:Readonly<Record<string,string|number|boolean>>
Counts, statuses, codes and hashes, such as { status: "transformed", queued: 2 }.
entryId
readonlyentryId:number
The entry's id in the audit log, which only grows.
messageId
readonlymessageId:number|undefined
The message acted on, when there is one.
outcome
readonlyoutcome:"ok"|"refused"|"not-found"
ok; refused, with detail.reason saying why; or not-found.
type
readonlytype:"audit"
AuditInput
An audit entry to append. The store adds the time and the actor.
Example
await store.appendAudit("noah", [{ action: "content-view", channel: "adt", messageId: 12 }]);
Properties
action
readonlyaction:"search"|"view"|"content-view"|"replay"|"resend"|"clear"|"retention"|"config"|"lease-release"|"migrate"|"channel-pause"|"channel-resume"|"channel-stop"|"channel-halt"|"channel-reload"|"verify"|"bulk-replay"|"archive"|"node-forget"
channel?
readonlyoptionalchannel?:string
detail?
readonlyoptionaldetail?:Readonly<Record<string,string|number|boolean>>
messageId?
readonlyoptionalmessageId?:number
outcome?
readonlyoptionaloutcome?:AuditOutcome
Defaults to ok.
AuditQuery
Which audit entries MessageStore.auditEntries returns: newest first, filters optional.
Example
const views = await store.auditEntries({ action: "content-view", since: "2026-11-01", limit: 100 });
Properties
action?
readonlyoptionalaction?:"search"|"view"|"content-view"|"replay"|"resend"|"clear"|"retention"|"config"|"lease-release"|"migrate"|"channel-pause"|"channel-resume"|"channel-stop"|"channel-halt"|"channel-reload"|"verify"|"bulk-replay"|"archive"|"node-forget"
before?
readonlyoptionalbefore?:number
Only ids below this one: the next of the page before.
channel?
readonlyoptionalchannel?:string
limit
readonlylimit:number
messageId?
readonlyoptionalmessageId?:number
since?
readonlyoptionalsince?:string
At or after this time (ISO 8601, UTC).
until?
readonlyoptionaluntil?:string
Before this time (ISO 8601, UTC).
ChannelContext
What a connector factory is given beside its options: the context, the channel's name and its format.
Example
context.logger.info({ event: "inbox-ready", channel: context.channel, format: context.format });
Extends
Properties
channel
readonlychannel:string
dir
readonlydir:string
The folder of the YAML file that names the extension: the channel's folder, or the project
root for pathways.yaml. Resolve relative paths in options against it.
Inherited from
env
readonlyenv:Readonly<Record<string,string|undefined>>
Where to read credentials and other settings from: process.env when the engine runs.
Inherited from
format
readonlyformat:"hl7v2"|"json"|"fhir"|"x12"|"ccda"|"text"|"csv"
logger
readonlylogger:ExtensionLogger
Inherited from
ContentStore
What pathways rekey needs from storage beyond MessageStore: every stored body and error
text as stored, and a way to replace them. The engine never uses it. Store implements it;
a storage adapter that implements it can be rekeyed.
Example
for (const row of await store.storedContent("messages", 0, 100)) {
await store.replaceContent(row, { body: reencrypted(row.body), error: row.error });
}
Methods
replaceContent()
replaceContent(
current,next):Promise<boolean>
Replaces a row's body and error with next, but only while the row still holds what current
read, so a change made meanwhile (a retry's new error) is never overwritten. Returns whether it
replaced them.
Parameters
current
next
body
Uint8Array
error
string | null
Returns
Promise<boolean>
storedContent()
storedContent(
table,after,limit):Promise<StoredContent[]>
Up to limit rows of table with an id above after, oldest first, as stored, leaving out the
rows of a message whose content was dropped: they hold none.
Parameters
table
after
number
limit
number
Returns
Promise<StoredContent[]>
CsvColumn
One column of a fixed-width csv channel: its name, the position of its first character counting
from 1, and how many characters it holds, all counted in decoded characters (code points).
Example
const column: CsvColumn = { name: "code", start: 1, length: 8 };
Properties
length
readonlylength:number
name
readonlyname:string
start
readonlystart:number
CsvSettings
How a csv channel reads and writes its bodies, from the channel's csv: block. Every key is
optional; see CSV_DEFAULTS for what an unset key means. With columns, the channel reads
fixed-width lines rather than delimited ones, and delimiter, quote, escape, header and
escapeFormulas do not apply.
Example
const settings: CsvSettings = { delimiter: "|", header: true, newline: "lf" };
Properties
columns?
readonlyoptionalcolumns?: readonlyCsvColumn[]
The columns of a fixed-width body. Unset for a delimited one.
delimiter?
readonlyoptionaldelimiter?:string
The one character between cells. Default ,.
encoding?
readonlyoptionalencoding?:"utf-8"|"latin1"
How the body's bytes are read and written: utf-8 (strict, the default) or latin1.
escape?
readonlyoptionalescape?:string
The one character that escapes a quote inside a quoted cell. Default: the quote, doubled.
escapeFormulas?
readonlyoptionalescapeFormulas?:boolean
Whether a cell that starts with =, +, -, @, a tab or a carriage return is written with a
leading ', quoted, so a spreadsheet does not run it as a formula. Default false.
header?
readonlyoptionalheader?:boolean
Whether the first line names the columns. Default true.
maxRowLength?
readonlyoptionalmaxRowLength?:number
The longest line read, in decoded characters. Default 1,048,576.
newline?
readonlyoptionalnewline?:"crlf"|"lf"|"cr"
The line ending written after a line a transform changed or added. Default: the first one the
body used, or crlf when it has none. Lines are read on CRLF, LF and a bare CR alike.
pad?
readonlyoptionalpad?:boolean
Whether a short line is padded (with empty cells, or spaces) rather than refused. Default false.
quote?
readonlyoptionalquote?:string
The one character a cell is quoted with. Default ".
DeliveredEvent
A destination accepted an output.
Example
if (event.type === "delivered" && event.attempt > 1) resolveIncident(event.channel, event.destination);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
messageId
readonlymessageId:number
Inherited from
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
type
readonlytype:"delivered"
Overrides
DeliveryEventBase
What every event about one delivery carries: the message, the destination and the delivery.
Example
const key = (event: DeliveryEventBase): string => `${event.channel}/${event.destination}/${event.deliveryId}`;
Extends
Extended by
DeliveredEventDeliveryExhaustedEventDeliveryRedirectedEventDeliveryRejectedEventDeliveryRetryEvent
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
channel
readonlychannel:string
The channel's name.
Inherited from
deliveryId
readonlydeliveryId:number
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
messageId
readonlymessageId:number
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
type
readonlytype:"delivered"|"received"|"receive-failed"|"transformed"|"filtered"|"transform-failed"|"delivery-retry"|"delivery-redirected"|"delivery-rejected"|"delivery-exhausted"|"source-down"|"source-up"|"audit"
Inherited from
DeliveryExhaustedEvent
A delivery ran out of retries: it had its retry.limit of attempts, or its next attempt would come
later than retry.maxAge after it was queued. It is marked failed, its body kept for an operator
to resend, and the destination moves on to the next. Only a destination whose retry settings set
a limit or an age sends this.
Example
if (event.type === "delivery-exhausted") page(`${event.channel}: gave up after ${event.attempt}`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The last error's code, such as ECONNREFUSED or HTTP_503.
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
messageId
readonlymessageId:number
Inherited from
name
readonlyname:string
The last error's name, such as MllpDeliveryError.
reason
readonlyreason:"limit"|"maxAge"
limit when it had its attempts, maxAge when it was too old to wait for another.
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
type
readonlytype:"delivery-exhausted"
Overrides
DeliveryRedirectedEvent
An HTTP destination answered with a redirect (a 3xx). Redirects are never followed, since following one could send a message to a host nobody configured: the delivery is held and tried again after the longest wait, five minutes, and its later deliveries wait behind it, until the destination's URL is fixed. It names the Location's host only, never its path or query.
Example
if (event.type === "delivery-redirected") page(`${event.channel} moved to ${event.locationHost}`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
locationHost
readonlylocationHost:string|undefined
The host the Location names, with its port when it has one, or undefined when it names none.
messageId
readonlymessageId:number
Inherited from
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
retryAt
readonlyretryAt:string
When the next attempt is due, as an ISO 8601 time in UTC.
retryInMs
readonlyretryInMs:number
How long until the next attempt, in milliseconds.
status
readonlystatus:number
The 3xx status, such as 301 or 308.
type
readonlytype:"delivery-redirected"
Overrides
DeliveryRejectedEvent
A destination rejected an output for good (an MLLP AE, an HTTP 4xx): it is marked failed and the
destination moves on to the next.
Example
if (event.type === "delivery-rejected") page(`${event.channel}: rejected (${event.reason})`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
messageId
readonlymessageId:number
Inherited from
reason
readonlyreason:string
The destination's short, stable reason, such as AE or HTTP 400.
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
type
readonlytype:"delivery-rejected"
Overrides
DeliveryRetryEvent
A delivery failed in a way worth trying again (a refused connection, a timeout, an MLLP AR, an
HTTP 408, 425, 429 or 5xx). The destination retries it after retryInMs, and its later deliveries
wait behind it. retryInMs is the backoff, or the wait a 429's or a 503's Retry-After asked for
when that is longer, up to five minutes.
Example
if (event.type === "delivery-retry" && event.attempt === 5) page(`${event.channel} is retrying`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
Which attempt this was, from 1.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The error's code, such as ECONNREFUSED, ACK_TIMEOUT or HTTP_503.
deliveryId
readonlydeliveryId:number
Inherited from
destination
readonlydestination:number
The destination's place in the channel's destinations: list, from 0.
Inherited from
messageId
readonlymessageId:number
Inherited from
name
readonlyname:string
The error's name, such as MllpDeliveryError.
resend?
readonlyoptionalresend?:object
Set when this attempt was a possible duplicate: an earlier one was cut off after the send, so
the receiver may have the message already. previousAttempts counts the attempts before it.
Absent otherwise.
previousAttempts
readonlypreviousAttempts:number
Inherited from
retryAt
readonlyretryAt:string
When the next attempt is due, as an ISO 8601 time in UTC.
retryInMs
readonlyretryInMs:number
How long until the next attempt, in milliseconds.
type
readonlytype:"delivery-retry"
Overrides
DestinationConnector
Sends messages out. Throw to fail an attempt: by default the engine retries it with growing waits
and holds this destination's later messages behind it. Throw a DeliveryRejected when
sending the same message again cannot succeed (the receiver rejected its content), and the engine
marks it failed and moves on. A custom destination's module builds one with a
DestinationFactory.
Example
const destination: DestinationConnector = {
async deliver(message) {
await upload(`${message.messageId}-${message.seq}`, message.body);
},
};
Methods
deliver()
deliver(
message):Promise<void>
Parameters
message
Returns
Promise<void>
stop()?
optionalstop():Promise<void>
Releases what the destination keeps open between deliveries, such as connections.
Returns
Promise<void>
ExpireResult
What retention deleted, or would delete, from one channel.
Example
const { messages, deliveries }: ExpireResult = await store.expire("adt", cutoff, true);
Properties
deliveries
readonlydeliveries:number
messages
readonlymessages:number
ExtensionContext
What every extension factory is given beside its options.
Example
const token = context.env["INBOX_TOKEN"];
const folder = path.resolve(context.dir, "./in");
Extended by
Properties
dir
readonlydir:string
The folder of the YAML file that names the extension: the channel's folder, or the project
root for pathways.yaml. Resolve relative paths in options against it.
env
readonlyenv:Readonly<Record<string,string|undefined>>
Where to read credentials and other settings from: process.env when the engine runs.
logger
readonlylogger:ExtensionLogger
ExtensionLogger
The engine's logger, handed to an extension. Lines land in the same JSON log as the engine's, tagged with the channel and which extension wrote them.
Example
context.logger.warn({ event: "inbox-read-failed", code: "EACCES" });
Methods
error()
error(
fields):void
Parameters
fields
Returns
void
info()
info(
fields):void
Parameters
fields
Returns
void
warn()
warn(
fields):void
Parameters
fields
Returns
void
FakeSecret
One version of a secret a fakeSecretProvider holds: its value, and optionally its version's
name (by default its place in the list, from "1"), when it expires and the identity that read it.
Example
const secret: FakeSecret = { value: "synthetic-token", version: "v2" };
Properties
expiresAt?
readonlyoptionalexpiresAt?:Date
identity?
readonlyoptionalidentity?:string
The principal it answers as having read it, as ResolvedSecret.identity.
value
readonlyvalue:string
version?
readonlyoptionalversion?:string
FakeSecretProvider
A SecretProvider held in memory, for tests: no network, no cloud account. It answers as the
contract asks of every provider, so a channel that reads { secret: { provider, name } } can be
tested against it in place of a cloud store.
Example
const vault = fakeSecretProvider({ "lab/api": '{"token":"synthetic-token"}' });
await vault.resolve({ name: "lab/api", field: "token" }, AbortSignal.timeout(1_000));
vault.calls; // [{ name: "lab/api", field: "token" }]
Extends
Properties
calls
readonlycalls: readonlySecretRef[]
Every request it was given, in order.
stopped
readonlystopped:number
How many times stop was called.
Methods
delete()
delete(
name):void
Removes name, so a later request for it is not found.
Parameters
name
string
Returns
void
fail()
fail(
name,error):void
Makes every later request for name reject with error, as a store that fails would.
Parameters
name
string
error
Error
Returns
void
resolve()
resolve(
ref,signal):Promise<ResolvedSecret>
Resolves one secret.
Parameters
ref
signal
AbortSignal
Returns
Promise<ResolvedSecret>
Inherited from
set()
set(
name,entry):void
Sets or replaces name; a single value or secret adds it as a new current version.
Parameters
name
string
entry
Returns
void
stop()?
optionalstop():void|Promise<void>
Releases what the provider holds open. Called once start has resolved every secret.
Returns
void | Promise<void>
Inherited from
FileSourceFacts
What a file source knows about a message: the file's name as it was dropped, its size in bytes, and when it was stored.
Example
const name = (source: FileSourceFacts): string => source.name;
Properties
kind
readonlykind:"file"
name
readonlyname:string
The file's name as it was dropped in the folder. It can carry a patient's name: never log it.
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
size
readonlysize:number
The body's size in bytes.
FilteredEvent
A message's transform returned no output, so nothing is delivered.
Example
if (event.type === "filtered") count("filtered", 1);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
messageId
readonlymessageId:number
type
readonlytype:"filtered"
Overrides
HookContext
What a hook factory is given beside its options: the context, and the channel for a hook a
channel's YAML names (undefined for an engine-wide one in pathways.yaml).
Example
const scope = context.channel ?? "engine";
Extends
Properties
channel
readonlychannel:string|undefined
dir
readonlydir:string
The folder of the YAML file that names the extension: the channel's folder, or the project
root for pathways.yaml. Resolve relative paths in options against it.
Inherited from
env
readonlyenv:Readonly<Record<string,string|undefined>>
Where to read credentials and other settings from: process.env when the engine runs.
Inherited from
logger
readonlylogger:ExtensionLogger
Inherited from
HttpSourceFacts
What an HTTP source knows about a message: the request's method, its path as the source matched
it, the sender's address, and the headers the channel's contextHeaders names, by lower-case
name. Authorization, Proxy-Authorization and Cookie are never among them.
Example
const requestId = (source: HttpSourceFacts): string | undefined => source.headers["x-request-id"];
Properties
headers
readonlyheaders:Readonly<Record<string,string>>
The listed headers the request carried, by lower-case name; several values joined by , .
kind
readonlykind:"http"
method
readonlymethod:string
POST or PUT.
path
readonlypath:string
The path the request was sent to, as the source matched it, without a query.
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
remoteAddress
readonlyremoteAddress:string|undefined
The sender's IP address, when the connection still had one.
KeyProvider
Where master keys live. Pathways encrypts each stored value with a fresh data key and asks the provider to wrap that key under its current master key; to read the value back, it asks the provider to unwrap it. A master key never has to leave the provider, so a cloud key service (AWS KMS, Azure Key Vault, Google Cloud KMS) can implement this interface by calling its own encrypt and decrypt. The built-in provider holds keys read from files or environment variables.
Rotation is the provider's to offer: it keeps unwrapping data keys under its older master keys while it wraps new ones under the current key.
Pathways overwrites a data key with zeros once it is done with it: wrapKey must not keep the
key it is given, and unwrapKey must return a buffer of its own.
Example
const kms: KeyProvider = {
currentKeyId: "kms-2026-11",
wrapKey: async (dataKey) => ({ keyId: "kms-2026-11", wrapped: await myKms.encrypt(dataKey) }),
unwrapKey: async (_keyId, wrapped) => myKms.decrypt(wrapped),
};
Properties
currentKeyId
readonlycurrentKeyId:string
The id of the master key that wraps new data keys.
keyIds?
readonlyoptionalkeyIds?: readonlystring[]
Every master key id the provider holds, the current one among them: a metadata search derives its index tags under each, so rows tagged under an older key are still found. Optional; without it, a search runs under the current key alone.
Methods
deriveIndexKey()?
optionalderiveIndexKey(keyId,info):Promise<Uint8Array<ArrayBufferLike>>
Derives a 32-byte key for one searchable index from the master key keyId, with HKDF-SHA-256
and info naming the index. Optional: a provider whose master keys never leave a key service
cannot derive one, and a channel with an index: block then refuses to start on it. The
returned buffer is the caller's, which overwrites it with zeros once it is done.
Parameters
keyId
string
info
Uint8Array
Returns
Promise<Uint8Array<ArrayBufferLike>>
unwrapKey()
unwrapKey(
keyId,wrapped):Promise<Uint8Array<ArrayBufferLike>>
Unwraps a data key that the master key keyId wrapped, returning its 32 bytes. Rejects with an
EncryptionError: ERR_PATHWAYS_UNKNOWN_KEY when it holds no key keyId,
ERR_PATHWAYS_INTEGRITY when the wrapped key fails its check.
Parameters
keyId
string
wrapped
Uint8Array
Returns
Promise<Uint8Array<ArrayBufferLike>>
wrapKey()
wrapKey(
dataKey):Promise<WrappedKey>
Wraps a 32-byte data key under the current master key.
Parameters
dataKey
Uint8Array
Returns
Promise<WrappedKey>
LeaseClaimant
Who asks for a lease: the node, an id each engine process draws at start, and its host name.
Example
const claimant: LeaseClaimant = { node: "3f9a1c2b", host: "engine-a" };
Properties
host
readonlyhost:string
node
readonlynode:string
LeaseHolder
A lease's last holder, by the database's clock: who took it, when, under which epoch, and how
long it has left. expiresInMs of 0 or less means it has lapsed or was released, and the next
acquisition takes it.
Example
const held = (holder: LeaseHolder): boolean => holder.expiresInMs > 0;
Properties
epoch
readonlyepoch:number
expiresInMs
readonlyexpiresInMs:number
host
readonlyhost:string
node
readonlynode:string
since
readonlysince:string
When it was taken, as an ISO 8601 time in UTC.
LifecycleEventBase
What every lifecycle event about a channel carries: its type, when it happened and the channel it happened on. An AuditEvent can concern no one channel, so it stands on its own. Events carry ids, counts, error names, codes and times, never message content and never an error's own text, so a hook can send them anywhere.
Example
const line = (event: LifecycleEventBase): string => `${event.at} ${event.channel} ${event.type}`;
Extended by
DeliveryEventBaseFilteredEventReceivedEventReceiveFailedEventSourceDownEventSourceUpEventTransformedEventTransformFailedEvent
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
channel
readonlychannel:string
The channel's name.
type
readonlytype:"delivered"|"received"|"receive-failed"|"transformed"|"filtered"|"transform-failed"|"delivery-retry"|"delivery-redirected"|"delivery-rejected"|"delivery-exhausted"|"source-down"|"source-up"|"audit"
LifecycleHook
A lifecycle hook: told of each LifecycleEvent, one at a time and in order. Paging, webhooks and forwarding to a SIEM are hooks.
A hook cannot hold the engine up. Each call has a time limit (the hook entry's timeout, 5
seconds unless set): past it, signal aborts, the call is abandoned and logged, and the hook gets
the next event. A call that throws is logged by error name and code, and the next event goes
ahead. Events queue for a busy hook; past 1,000 waiting, new ones are dropped and logged. On stop,
the engine gives a hook up to its time limit to finish what is queued, then calls stop.
Hooks run in the engine's own thread: an endless synchronous loop stops everything, so a hook awaits its I/O and does little work itself.
Example
// A webhook, much as the built-in `webhook:` hook does it.
import type { HookFactory } from "@cosyte/pathways";
export default ((options, context) => ({
async onEvent(event, signal) {
const response = await fetch(options.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${context.env["OPS_WEBHOOK_TOKEN"] ?? ""}`,
},
body: JSON.stringify(event),
signal,
});
if (!response.ok) throw Object.assign(new Error("webhook refused"), { code: `HTTP_${response.status}` });
},
})) satisfies HookFactory<{ url: string }>;
Methods
onEvent()
onEvent(
event,signal):void|Promise<void>
Handles one event. Resolve when done; reject to have the failure logged. signal aborts when
the call runs past its time limit.
Parameters
event
signal
AbortSignal
Returns
void | Promise<void>
stop()?
optionalstop():void|Promise<void>
Releases what the hook keeps open, such as connections. Called once, after the last event.
Returns
void | Promise<void>
LogFields
One structured log line: an event name, then fields that are ids, counts, codes and error
names. Never message content, never an error's own text, never a credential.
Example
const fields: LogFields = { event: "inbox-read-failed", code: "EACCES" };
Indexable
[
field:string]:LogValue
Properties
event
readonlyevent:string
MessageQuery
Which messages MessageStore.listMessages returns: newest first, every filter optional.
Example
const query: MessageQuery = { channel: "adt", state: "failed", since: "2026-11-01T00:00:00.000Z", limit: 50 };
Properties
before?
readonlyoptionalbefore?:number
Only ids below this one: the next of the page before.
channel?
readonlyoptionalchannel?:string
ids?
readonlyoptionalids?: readonlynumber[]
Only these messages, by id.
limit
readonlylimit:number
since?
readonlyoptionalsince?:string
Received at or after this time (ISO 8601, UTC).
state?
readonlyoptionalstate?:"delivered"|"received"|"filtered"|"failed"|"cleared"|"dry-run"|"queued"|"retrying"
until?
readonlyoptionaluntil?:string
Received before this time (ISO 8601, UTC).
MessageRows
One message's row and every delivery row of it, oldest first: what retention archives before it
changes or deletes them. The index tags of its metadata are left out: they are derived from
meta under the master keys, and are derived again from it.
Example
const rows: MessageRows | undefined = await store.archiveRows(12);
Properties
deliveries
readonlydeliveries: readonlyStoredDeliveryRow[]
message
readonlymessage:StoredMessageRow
MessageSink
What a source hands each incoming message to, and tells when its connection drops and comes back.
Example
const id = await sink.receive(body); // durable once this resolves
await acknowledge(id);
socket.on("close", () => {
const wait = sink.sourceDown?.("ECONNRESET") ?? 1_000;
setTimeout(reconnect, wait);
});
socket.on("ready", () => sink.sourceUp?.());
Methods
receive()
receive(
body,options?):Promise<number>
Stores the message and resolves with its id once it is durable. Acknowledge the sender (move the file, send the ACK) only after this resolves: that ordering is what keeps a crash from losing a message. If it rejects, do not acknowledge; the sender should send again.
options is optional, so a source that passes only body keeps working unchanged.
On a channel that stores nothing (storage.content: none), nothing is stored: this resolves
once the message was transformed and every destination accepted it, with its pass id (the
time it came in, in microseconds since 1970, never given twice), and rejects with a PassFailed that says whether the sender should send again.
Parameters
body
Uint8Array
options?
Returns
Promise<number>
sourceDown()?
optionalsourceDown(code):number
Says the source lost what it reads from (a broker, a remote server, a socket it dialled) and
cannot take messages in until it is back. Until sourceUp, the health endpoint shows
the channel's source down and answers 503, readiness fails, and the hooks are sent a
source-down event with name: "SourceDown". code is a short code such as ECONNRESET
(letters, digits and _.:-, at most 64 characters), never an error's text: anything else is
kept as UNSPECIFIED.
Returns how long to wait before trying to connect again, in milliseconds: 1 second, doubling with each report in a row, to 5 minutes. The engine's own sink always has this method; it is optional so a sink a test builds by hand need not.
Parameters
code
string
Returns
number
sourceUp()?
optionalsourceUp():void
Says a source that reported sourceDown is connected again: health shows it up, and
the hooks are sent a source-up event. Nothing happens when it was not down.
Returns
void
MessageStore
What the engine and the operator commands need from storage. Store implements it on
SQLite, PostgreSQL, MySQL or MariaDB, and SQL Server; a storage adapter for anything else
implements the same methods, and its module's StoreFactory returns it. A layer around a store
(encryption at rest) wraps them: every method that takes or returns a body or an error's text is
here, so a wrapping layer sees them all.
Every method is the engine's only path to that data, so an adapter keeps these promises: receive
is durable before it resolves; completeTransform writes all its deliveries and the message's new
status, or none of them; nextDelivery returns the lowest pending id for that destination, so
deliveries stay in order; a new delivery is due at once (nextAttemptAt 0); markSending is
durable before it resolves, nextDelivery returns the marker it wrote, and markDelivered,
scheduleRetry and failDelivery clear it in the same write as their outcome. For the operator
commands: requeue and clear change nothing unless the message still stands where the caller
found it, and write their audit entry in the same transaction as their change; expire deletes
only done messages, with their deliveries, and never an audit entry. A message whose content was
dropped (markDeliveredAndDropContent) is never replayed, resent or expired.
The audit log has two methods, append and read, and no way to change or remove an entry.
The lease methods are optional. A store with acquireLease, renewLease, releaseLease and
leaseHolder lets pathways start refuse a second engine and run a standby; each judges expiry
by one clock, the database's, in the same statement as its change. Given an epoch, the fenced
writes (completeTransform, failTransform, markSending, markDelivered, scheduleRetry and
failDelivery) change their row only while the engine lease still carries that epoch, in the
same statement, and reject with a LeaseLostError when they change nothing. A store
without the lease methods runs as before, with a warning at start, and cannot run a standby.
Example
class PoolStore implements MessageStore {
async receive(channel: string, body: Uint8Array, options?: StoreReceiveOptions) {
const id = await this.pool.insertMessage(channel, body, options?.failure);
return { id, duplicateOf: undefined };
}
// ...and the other methods, each against the same database.
}
Methods
acquireLease()?
optionalacquireLease(name,claimant,ttlMs):Promise<LeaseClaim>
Takes the lease name for ttlMs when it is free or has lapsed by the database's clock,
raising its epoch by one, in one statement; else answers with its holder. Optional.
Parameters
name
string
claimant
ttlMs
number
Returns
Promise<LeaseClaim>
appendAudit()
appendAudit(
actor,entries):Promise<AuditEntry[]>
Appends entries to the audit log, stamped with the time and actor.
Parameters
actor
string
entries
readonly AuditInput[]
Returns
Promise<AuditEntry[]>
auditEntries()
auditEntries(
query):Promise<Page<AuditEntry>>
A page of the audit log, newest first.
Parameters
query
Returns
Promise<Page<AuditEntry>>
channels()
channels():
Promise<string[]>
Every channel with a stored message, by name.
Returns
Promise<string[]>
channelStates()?
optionalchannelStates():Promise<Record<string,ChannelRunState>>
Every channel an operator paused or stopped, by name; a channel missing from it runs. Optional, with setChannelState: a store without them keeps a channel's state in the engine only, so a restart forgets it.
Returns
Promise<Record<string, ChannelRunState>>
clear()
clear(
messageId,actor):Promise<ClearResult>
Marks a failed message, and its deliveries that failed for good, cleared. Refuses when there
is nothing failed. The audit entry is written in the same transaction.
Parameters
messageId
number
actor
string
Returns
Promise<ClearResult>
close()
close():
Promise<void>
Returns
Promise<void>
completeTransform()
completeTransform(
messageId,outputs,destinations,epoch?):Promise<void>
Fenced by epoch when it is given: see the lease methods above.
Parameters
messageId
number
outputs
readonly Uint8Array<ArrayBufferLike>[]
destinations
number
epoch?
number
Returns
Promise<void>
deliveries()
deliveries(
messageId):Promise<StoredDelivery[]>
A message's deliveries, every generation, oldest first, without bodies.
Parameters
messageId
number
Returns
Promise<StoredDelivery[]>
deliveryCounts()
deliveryCounts(
channel):Promise<DeliveryCounts>
How many deliveries of a channel stand at each status; dry-run only when some ran dry.
Parameters
channel
string
Returns
Promise<DeliveryCounts>
expire()
expire(
channel,cutoff,dryRun):Promise<ExpireResult>
Deletes a channel's messages that are done (delivered, dry-run, filtered or cleared) and whose
last activity is before cutoff (ISO 8601), with their deliveries, leaving those whose content
was dropped. With dryRun, counts them only.
Parameters
channel
string
cutoff
string
dryRun
boolean
Returns
Promise<ExpireResult>
failDelivery()
failDelivery(
deliveryId,error,epoch?):Promise<void>
Fenced by epoch when it is given.
Parameters
deliveryId
number
error
string
epoch?
number
Returns
Promise<void>
failTransform()
failTransform(
messageId,error,epoch?):Promise<void>
Fenced by epoch when it is given.
Parameters
messageId
number
error
string
epoch?
number
Returns
Promise<void>
forgetNode()?
optionalforgetNode(node):Promise<boolean>
Removes the row of node only while it is stale. Returns whether it did. Optional.
Parameters
node
string
Returns
Promise<boolean>
joinNode()?
optionaljoinNode(info,replace?):Promise<NodeJoin>
Writes this node's row, its join time and first heartbeat from the database's clock: a new row,
or over a row of the same id that is stale, or that still names replace's host and process
and has not beaten for replace.silentMs (an earlier run of this node whose heartbeat stopped).
Answers with the live row that holds the id instead. Optional, with nodeHeartbeat,
leaveNode, nodes and forgetNode.
Parameters
info
replace?
Returns
Promise<NodeJoin>
leaseHolder()?
optionalleaseHolder(name):Promise<LeaseHolder|undefined>
The lease's last holder, or undefined when it was never taken. Optional, with acquireLease.
Parameters
name
string
Returns
Promise<LeaseHolder | undefined>
leaveNode()?
optionalleaveNode(self):Promise<boolean>
Removes the node's row while it still names self, at a clean stop. Optional.
Parameters
self
Returns
Promise<boolean>
listMessages()
listMessages(
query):Promise<Page<MessageSummary>>
A page of messages, newest first, without bodies or errors.
Parameters
query
Returns
Promise<Page<MessageSummary>>
listRetrying()
listRetrying(
channel,limit):Promise<RetryingDelivery[]>
A channel's retrying deliveries, oldest first, at most limit.
Parameters
channel
string
limit
number
Returns
Promise<RetryingDelivery[]>
markDelivered()
markDelivered(
deliveryId,epoch?):Promise<void>
Fenced by epoch when it is given.
Parameters
deliveryId
number
epoch?
number
Returns
Promise<void>
markDeliveredAndDropContent()?
optionalmarkDeliveredAndDropContent(deliveryId,epoch?):Promise<boolean>
markDelivered, and when that was the last delivery of the message's current generation
still to be delivered, also drops the bodies of the message and of all its deliveries in the
same transaction, keeping its metadata: ids, times, statuses, sizes, the content hash and the
possible-duplicate flag. Its ctx.meta and that metadata's index tags go with the bodies, since
they hold identifiers. Keeps every body while any delivery of the current generation has
failed or waits to be tried again. Resolves to whether it dropped them. Fenced by epoch when
it is given. Optional: a channel with storage.content: dropAfterDelivery needs it, and the
engine refuses to start such a channel on a store without it.
Parameters
deliveryId
number
epoch?
number
Returns
Promise<boolean>
markDryRun()?
optionalmarkDryRun(deliveryId,epoch?):Promise<void>
Marks a delivery dry-run: its output stays recorded, it was never sent, and its destination
moves on to the next. Clears the marker. Fenced by epoch when it is given. Optional: an engine
with a destination that runs dry refuses to start on a store without it.
Parameters
deliveryId
number
epoch?
number
Returns
Promise<void>
markSending()
markSending(
deliveryId,start,epoch?):Promise<void>
Writes the marker before a delivery is handed to its destination, in one write: the attempt and
its start, and the attempt found cut off when start.interruptedAttempt is set. A delivery
nextDelivery returns still marked was cut off, and its next send is flagged. Fenced by
epoch when it is given.
Parameters
deliveryId
number
start
epoch?
number
Returns
Promise<void>
message()
message(
id):Promise<StoredMessage|undefined>
One message, without its body, or undefined if there is none. Its error is raw: see there.
Parameters
id
number
Returns
Promise<StoredMessage | undefined>
messageBody()
messageBody(
id):Promise<Uint8Array<ArrayBufferLike> |undefined>
One message's body as it arrived, or undefined if there is none or it was dropped.
Parameters
id
number
Returns
Promise<Uint8Array<ArrayBufferLike> | undefined>
messageCounts()
messageCounts(
channel):Promise<Record<MessageStatus,number>>
How many messages of a channel stand at each status. The health endpoint reports these.
Parameters
channel
string
Returns
Promise<Record<MessageStatus, number>>
metaCandidates()?
optionalmetaCandidates(query):Promise<MetaCandidate[]>
Up to query.limit messages that carry the tags query names, newest first, each with its
metadata as stored, for a metadata search to confirm. Optional: a store without it cannot be
searched by metadata.
Parameters
query
Returns
Promise<MetaCandidate[]>
nextDelivery()
nextDelivery(
channel,destination,options?):Promise<PendingDelivery|undefined>
Parameters
channel
string
destination
number
options?
Returns
Promise<PendingDelivery | undefined>
nextToTransform()
nextToTransform(
channel):Promise<PendingMessage|undefined>
Parameters
channel
string
Returns
Promise<PendingMessage | undefined>
nodeHeartbeat()?
optionalnodeHeartbeat(self,state):Promise<boolean>
One UPDATE: sets the node's heartbeat from the database's clock in the same statement, with its
role and draining flag, while the row still names self. Returns whether it did. Optional.
Parameters
self
state
Returns
Promise<boolean>
nodes()?
optionalnodes():Promise<NodeRow[]>
Every node row, by node id, with its heartbeat age by the database's clock. Optional.
Returns
Promise<NodeRow[]>
outputs()
outputs(
messageId):Promise<Uint8Array<ArrayBufferLike>[]>
The outputs of a message's current generation, in order; none when it has none or they were dropped.
Parameters
messageId
number
Returns
Promise<Uint8Array<ArrayBufferLike>[]>
receive()
receive(
channel,body,options?):Promise<ReceivedMessage>
Stores an incoming message, durably, before its source acknowledges it: as received, or as
failed when options.failure is set. Flags it as a possible duplicate when an earlier
message of the channel has the same options.duplicateKey. Given options.resumed, when a
message of the channel was already stored with the same options.idempotencyKey, stores
nothing and returns that message's id with alreadyStored: true. A store that ignores the key
still works, and a source that stops between storing and acknowledging then stores that
message twice.
Parameters
channel
string
body
Uint8Array
options?
Returns
Promise<ReceivedMessage>
releaseLease()?
optionalreleaseLease(name,epoch?):Promise<boolean>
Frees the lease name, so the next acquisition takes it at once: only while it carries epoch
when that is given, whoever holds it when not (pathways lease release --force). Returns
whether it freed it. Optional, with acquireLease.
Parameters
name
string
epoch?
number
Returns
Promise<boolean>
renewLease()?
optionalrenewLease(name,epoch,ttlMs):Promise<boolean>
Extends the lease name to ttlMs from now, by the database's clock, while it still carries
epoch and has not lapsed. Returns whether it did. Optional, with acquireLease.
Parameters
name
string
epoch
number
ttlMs
number
Returns
Promise<boolean>
requeue()
requeue(
messageId,request,actor):Promise<RequeueResult>
Records a replay or a resend: the message's new status, a new generation, and fresh deliveries at the back of each destination's queue. Refuses, changing nothing, when the message has moved since the caller read it or has deliveries still pending. The audit entry is written in the same transaction, whichever way it goes.
Parameters
messageId
number
request
actor
string
Returns
Promise<RequeueResult>
retryingDeliveries()
retryingDeliveries(
channel):Promise<number>
How many of a channel's pending deliveries have failed at least once and wait for a retry.
Parameters
channel
string
Returns
Promise<number>
scheduleRetry()
scheduleRetry(
deliveryId,attempts,nextAttemptAt,error,epoch?):Promise<void>
Fenced by epoch when it is given.
Parameters
deliveryId
number
attempts
number
nextAttemptAt
number
error
string
epoch?
number
Returns
Promise<void>
setChannelState()?
optionalsetChannelState(channel,state):Promise<void>
Sets a channel's state; running removes its row. Optional, with channelStates.
Parameters
channel
string
state
Returns
Promise<void>
MessageSummary
What storage holds about one message, without its body or its error: safe to list.
Example
const { items } = await store.listMessages({ state: "failed", limit: 20 });
const ids = items.map((message: MessageSummary) => message.id);
Extended by
Properties
channel
readonlychannel:string
contentDroppedAt?
readonlyoptionalcontentDroppedAt?:string
When the message's body and its deliveries' bodies were dropped, once its last delivery was
delivered (storage.content: dropAfterDelivery). Undefined while they are kept. Such a
message cannot be replayed or resent, and its content cannot be shown.
contentHash?
readonlyoptionalcontentHash?:string
The hash of the body as received, when one was stored with it: see StoreReceiveOptions.
deliveries
readonlydeliveries:DeliveryTally
The deliveries of the current generation, by status.
duplicateOf
readonlyduplicateOf:number|null
The latest earlier message with the same sender and control ID, when there is one.
generation
readonlygeneration:number
0 until the message is first replayed or resent; one more each time.
id
readonlyid:number
receivedAt
readonlyreceivedAt:string
size
readonlysize:number|null
The body's size in bytes, or null for a message stored before sizes were kept.
state
readonlystate:"delivered"|"received"|"filtered"|"failed"|"cleared"|"dry-run"|"queued"|"retrying"
The status and the current deliveries, as an operator sees them.
status
readonlystatus:MessageStatus
updatedAt
readonlyupdatedAt:string
When the message last changed: transformed, failed, replayed or cleared.
MetaCandidate
A message whose index tags match a search, and its metadata as stored, which the search decrypts to confirm the match: tags are truncated, so a row can carry a tag and hold another value.
Example
const candidate: MetaCandidate = { id: 12, meta: storedJson };
Properties
id
readonlyid:number
meta
readonlymeta:string
MetaCandidateQuery
Which messages MessageStore.metaCandidates returns: those of channel that carry, for
every condition, one of its tags, newest first, with the other filters of a listing.
Example
const query: MetaCandidateQuery = { channel: "adt", conditions: [condition], limit: 100 };
Properties
before?
readonlyoptionalbefore?:number
Only ids below this one.
channel
readonlychannel:string
conditions
readonlyconditions: readonlyMetaCondition[]
limit
readonlylimit:number
since?
readonlyoptionalsince?:string
Received at or after this time (ISO 8601, UTC).
state?
readonlyoptionalstate?:"delivered"|"received"|"filtered"|"failed"|"cleared"|"dry-run"|"queued"|"retrying"
until?
readonlyoptionaluntil?:string
Received before this time (ISO 8601, UTC).
MetaCondition
What a metadata search looks up for one condition: the index, and its tag under each master key id the provider holds. A row matches the condition when it carries any of them.
Example
const condition: MetaCondition = { index: "mrn", tags: [{ keyId: "prod-2026", tag: "a3f0" }] };
Properties
index
readonlyindex:string
tags
readonlytags: readonlyobject[]
MetaStore
What pathways rekey needs from storage to rewrite metadata: every message's metadata and tags,
and a way to replace them. Store implements it.
Example
for (const row of await store.storedMeta(0, 100)) await store.replaceMeta(row, next(row));
Methods
replaceMeta()
replaceMeta(
current,next):Promise<boolean>
Replaces a message's metadata and all its tags with next, in one transaction, only while the
metadata is still what current read. Returns whether it did.
Parameters
current
next
meta
string
tags
readonly MetaTag[]
Returns
Promise<boolean>
storedMeta()
storedMeta(
after,limit):Promise<StoredMeta[]>
Up to limit messages with metadata and an id above after, oldest first, as stored.
Parameters
after
number
limit
number
Returns
Promise<StoredMeta[]>
MetaTag
One index tag, as storage keeps it: the index (the metadata key it covers), the master key id it
was derived under, and the tag, the first bits bits of an HMAC-SHA-256 in lowercase hex.
Example
const tag: MetaTag = { index: "mrn", keyId: "prod-2026", tag: "a3f0" };
Properties
index
readonlyindex:string
keyId
readonlykeyId:string
tag
readonlytag:string
MllpSourceFacts
What an MLLP source knows about a message: the sender's address and, over mutual TLS, the common name in the subject of the certificate the sender presented.
Example
const peer = (source: MllpSourceFacts): string => source.tlsPeerSubject ?? "unknown";
Properties
kind
readonlykind:"mllp"
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
remoteAddress
readonlyremoteAddress:string|undefined
The sender's IP address, when the connection had one.
tlsPeerSubject
readonlytlsPeerSubject:string|undefined
The common name (CN) in the subject of the sender's TLS certificate, when it presented one.
ModuleSourceFacts
What a custom source knows about a message: whatever it passed as source to sink.receive.
Example
const queue = (source: ModuleSourceFacts): JsonValue | undefined => source.facts["queue"];
Properties
facts
readonlyfacts:Readonly<Record<string,JsonValue>>
What the source passed, or {} when it passed nothing.
kind
readonlykind:"module"
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
NextDeliveryOptions
What MessageStore.nextDelivery looks for. Without dueBy it returns the oldest pending
delivery, due or not, which keeps a destination in order.
Example
const due = await store.nextDelivery("adt", 0, { dueBy: Date.now() });
Properties
dueBy?
readonlyoptionaldueBy?:number
Only a delivery due by this time, in epoch milliseconds: the oldest whose nextAttemptAt is at
or before it, passing over any still waiting for a retry. The engine asks this for a destination
with retry.rotate: true. A store that ignores it keeps that destination in order.
NodeInfo
Everything a node writes when it joins: who it is, which Pathways and schema generation it runs, where it stands, and how long after its last heartbeat it counts as stale. Ids, a host name, numbers and states only: never content, a path or a URL.
Example
const info: NodeInfo = {
node: "engine-a", host: "host-a", pid: 4242, version: "0.0.2", schemaGeneration: 11,
role: "active", draining: false, staleAfterMs: 15_000,
};
Extends
Extended by
Properties
draining
readonlydraining:boolean
Inherited from
host
readonlyhost:string
Inherited from
node
readonlynode:string
Inherited from
pid
readonlypid:number
Inherited from
role
readonlyrole:"single"|"standby"|"active"|"proving"|"draining"|"fenced"
Inherited from
schemaGeneration
readonlyschemaGeneration:number|null
The schema generation that version brings a built-in store to; null for a custom store.
staleAfterMs
readonlystaleAfterMs:number
How long after its last heartbeat the node counts as stale, in milliseconds.
version
readonlyversion:string
The version of @cosyte/pathways the node runs.
NodeReplace
The row a join may replace: an earlier run of this node, named by its host and process, and how
long its heartbeat must have been silent, by the database's clock, for the row to be taken. A
row that names another process, or that beat within silentMs, is not.
Example
const replace: NodeReplace = { node: "engine-a", host: "host-a", pid: 1, silentMs: 7_500 };
Extends
Properties
host
readonlyhost:string
Inherited from
node
readonlynode:string
Inherited from
pid
readonlypid:number
Inherited from
silentMs
readonlysilentMs:number
How long the row's heartbeat must have been silent, in milliseconds.
NodeRow
A node row as read, by the database's clock: its info, when it joined, how long ago its last heartbeat was, and whether that is longer ago than its own stale bound.
Example
const alive = (row: NodeRow): boolean => !row.stale;
Extends
Properties
draining
readonlydraining:boolean
Inherited from
heartbeatAgeMs
readonlyheartbeatAgeMs:number
How long ago the last heartbeat was, in milliseconds, by the database's clock.
host
readonlyhost:string
Inherited from
node
readonlynode:string
Inherited from
pid
readonlypid:number
Inherited from
role
readonlyrole:"single"|"standby"|"active"|"proving"|"draining"|"fenced"
Inherited from
schemaGeneration
readonlyschemaGeneration:number|null
The schema generation that version brings a built-in store to; null for a custom store.
Inherited from
stale
readonlystale:boolean
True once the last heartbeat is older than staleAfterMs.
staleAfterMs
readonlystaleAfterMs:number
How long after its last heartbeat the node counts as stale, in milliseconds.
Inherited from
startedAt
readonlystartedAt:string
When the node joined, as an ISO 8601 time in UTC.
version
readonlyversion:string
The version of @cosyte/pathways the node runs.
Inherited from
NodeSelf
Which process a node row belongs to: its node id, host name and process id. A heartbeat or a leave changes the row only while it still names all three, so an earlier process that ran under the same id cannot touch the row of the one that replaced it.
Example
const self: NodeSelf = { node: "engine-a", host: "host-a", pid: 4242 };
Extended by
Properties
host
readonlyhost:string
node
readonlynode:string
pid
readonlypid:number
NodeState
What a heartbeat writes beside the database's time: the node's role and its draining flag.
Example
const state: NodeState = { role: "active", draining: false };
Extended by
Properties
draining
readonlydraining:boolean
role
readonlyrole:"single"|"standby"|"active"|"proving"|"draining"|"fenced"
OutgoingMessage
One output of one message, on its way to one destination.
Example
const name = (message: OutgoingMessage): string => `${message.messageId}-${message.seq}.hl7`;
Properties
body
readonlybody:Uint8Array
messageId
readonlymessageId:number
The id of the stored message this came from; on a channel that stores nothing, the message's pass id: the time it came in, in microseconds since 1970, never given twice, so outputs named by it stay apart across restarts.
resend?
readonlyoptionalresend?:Resend
Set when this is a possible duplicate: an earlier attempt was cut off after the send, so the
receiver may have it already. A destination may tell the receiver so (an HTTP destination's
resendHeader); it sends the message either way. Undefined on every other send.
seq
readonlyseq:number
Which output of that message's transform this is, from 0.
signal?
readonlyoptionalsignal?:AbortSignal
Aborted when an operator halts the channel (pathways channel halt) while this send is in
flight. A destination that honours it gives up the send at once; the engine stops waiting
either way, records no outcome, and sends the message again after the channel resumes, flagged
as a possible duplicate when the send was marked.
Page
One page of results, newest first. next is the before that fetches the page after it.
Example
const first: Page<MessageSummary> = await store.listMessages({ limit: 50 });
const second = await store.listMessages({ limit: 50, before: first.next });
Type Parameters
T
T
Properties
items
readonlyitems: readonlyT[]
next
readonlynext:number|undefined
PendingDelivery
The oldest delivery still pending for one destination of one channel.
Example
const due = (delivery: PendingDelivery): boolean => delivery.nextAttemptAt <= Date.now();
Properties
attempts
readonlyattempts:number
body
readonlybody:Uint8Array
id
readonlyid:number
interruptedAttempt?
readonlyoptionalinterruptedAttempt?:number
The latest attempt found cut off, kept after it: see StoredDelivery.interruptedAttempt.
messageId
readonlymessageId:number
nextAttemptAt
readonlynextAttemptAt:number
Epoch milliseconds before which this delivery must not be tried again.
sending?
readonlyoptionalsending?:SendingMarker
The marker of an attempt whose outcome was never recorded: set means it was cut off after it was handed to the destination, which may have it. Undefined when there is none, and from a store that keeps no marker.
seq
readonlyseq:number
PendingMessage
A stored message waiting for its transform.
Example
const next: PendingMessage | undefined = await store.nextToTransform("adt");
Properties
body
readonlybody:Uint8Array
id
readonlyid:number
ReceivedEvent
A message was stored, before its source acknowledged it.
Example
if (event.type === "received" && event.duplicateOf !== undefined) flagDuplicate(event.messageId);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
bytes
readonlybytes:number
The message's size in bytes.
channel
readonlychannel:string
The channel's name.
Inherited from
duplicateOf
readonlyduplicateOf:number|undefined
The earlier message this one may repeat (same sender and control ID), when there is one.
failed
readonlyfailed:boolean
True when the source stored it as failed, such as an MLLP message with no usable header.
messageId
readonlymessageId:number
type
readonlytype:"received"
Overrides
ReceivedMessage
An incoming message, stored.
Example
const { id, duplicateOf } = await store.receive("adt", body);
Properties
alreadyStored?
readonlyoptionalalreadyStored?:boolean
True when a message of the channel was already stored with the same idempotency key: id is
that message's, and nothing new was stored.
duplicateOf
readonlyduplicateOf:number|undefined
The latest earlier message of the channel with the same duplicate key, when there is one.
id
readonlyid:number
ReceiveFailedEvent
A message could not be stored, so its source did not acknowledge it and its sender should send it again. Storage being unreachable shows up here first.
Example
if (event.type === "receive-failed") page(`storage refused a message on ${event.channel}`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The error's code, when it has one, such as SQLITE_FULL.
name
readonlyname:string
The error's name, such as SqliteError.
type
readonlytype:"receive-failed"
Overrides
ReceiveOptions
What a source can tell the engine about a message it hands over. Every field is optional.
Example
await sink.receive(body, { duplicateKey: `${sender}|${controlId}` });
await sink.receive(body, { failure: "inbound: NO_MSH_SEGMENT" });
await sink.receive(body, { idempotencyKey: `queue|${deliveryTag}` });
await sink.receive(body, { idempotencyKey: `queue|${deliveryTag}`, resumed: true });
await sink.receive(body, { source: { queue: "inbound", deliveryTag } });
Properties
duplicateKey?
readonlyoptionalduplicateKey?:string
What identifies the message to its sender, such as the sender and its MSH-10 control ID. When an earlier message of the channel carried the same key, this one is stored and processed as usual, flagged as a possible duplicate: never dropped. The engine keeps a hash of the key, never the key itself.
failure?
readonlyoptionalfailure?:string
Stores the message as failed, with this reason, instead of handing it to the transform: for a message the source received but could not accept, such as one with no usable header. A short code, never message content.
idempotencyKey?
readonlyoptionalidempotencyKey?:string
What names this one message to the source, the same every time the source hands it in. It is
stored with the message, so that when the source hands the message in again with resumed,
after a stop between storing it and acknowledging it, it is stored once. The engine keeps a
hash of the key, never the key itself. A custom store may ignore it, and then stores such a
message twice.
resumed?
readonlyoptionalresumed?:boolean
True when the source hands in again, under the same idempotencyKey, a message that an earlier
run may have stored already: one it took in and never acknowledged. When a message of the
channel was stored with that key, nothing new is stored and that message's id comes back. Only
these messages are looked up, so a message handed in for the first time costs no lookup.
source?
readonlyoptionalsource?:Readonly<Record<string,JsonValue>>
What the source knows about the message, which the channel's transform reads as
ctx.source.facts: a map of plain JSON values (strings, finite numbers, booleans, null, lists
and maps). Anything else is refused with the code INVALID_SOURCE_FACTS before the message is
stored. The engine keeps a copy taken at the call, so the source may reuse or change its object
afterwards. It is never logged, sent to a hook or stored: the engine holds it in memory until the
message is transformed, so a message taken in before the engine restarts is transformed
without it.
RequeueRequest
What a replay or a resend asks storage to record. Storage checks the message still stands where the caller found it, and changes nothing otherwise.
Example
const request: RequeueRequest = { mode: "resend", expected: "transformed", outputs, destinations: 2 };
Properties
destinations
readonlydestinations:number
How many destinations the channel has now.
expected
readonlyexpected:MessageStatus
The status the caller read before transforming.
expectedGeneration?
readonlyoptionalexpectedGeneration?:number
The generation the caller read. When set, storage also refuses with changed when the message
was replayed or resent since, so a bulk replay never replays a message twice.
failure?
readonlyoptionalfailure?:string
What the replayed transform failed with, as stored; the message is marked failed.
mode
readonlymode:"replay"|"resend"
replay ran the transform again; resend queues the stored outputs again.
outputs
readonlyoutputs: readonlyUint8Array<ArrayBufferLike>[]
The outputs to queue, in order, for each of destinations. None, without a failure, means the
transform filtered the message out.
Resend
Set on a message that is being sent again after an attempt was cut off: the engine stopped after handing it to the destination and before it recorded the outcome, so the receiver may have it already. The message is the same; only the flag tells it apart.
Example
const header = (resend: Resend | undefined): string | undefined =>
resend === undefined ? undefined : String(resend.previousAttempts);
Properties
previousAttempts
readonlypreviousAttempts:number
How many attempts were made before this one, the one cut off among them.
ResolvedSecret
What a provider answers: the value, the version it is, and when it stops being good, if it does.
Example
const secret: ResolvedSecret = { value: "synthetic-token", version: "3", expiresAt: new Date("2026-12-01T00:00:00Z") };
Properties
expiresAt?
readonlyoptionalexpiresAt?:Date
When the value stops being good, for a lease or a short-lived credential.
identity?
readonlyoptionalidentity?:string
Who read it, when the provider can tell: the cloud principal its call ran as, such as a role's
ARN, a managed identity's id or a service account's email. Never secret. pathways secrets check prints it, when it is printable ASCII with no space, 1 to 256 characters.
value
readonlyvalue:string
The secret itself. Pathways never logs it, and never puts it in an error.
version
readonlyversion:string
The version resolved, as the provider names it; never secret.
RetentionQuery
Which messages one retention step reaches: those of its kind whose last activity is before
cutoff.
Example
const query: RetentionQuery = { step: "metadata", cutoff: "2026-08-01T00:00:00.000Z", includeDropped: true };
Properties
cutoff
readonlycutoff:string
ISO 8601, UTC.
includeDropped?
readonlyoptionalincludeDropped?:boolean
For the metadata step: also delete a message whose content was dropped, after delivery or by
the content step. Without it such a message is kept, as retention: 30d always has.
step
readonlystep:RetentionStep
RetentionStore
What retention needs from storage to archive before it changes anything: the messages a step would reach, a batch at a time, every column of each, and a way to change exactly those. Each method is optional on MessageStore; Store has all four.
Example
const ids = await store.listExpiring("adt", query, 500);
await store.applyExpiry("adt", ids, query);
Methods
applyExpiry()
applyExpiry(
channel,ids,query):Promise<ExpireResult>
Applies query's step to those of ids it still reaches, in one transaction, re-checking each,
so a message replayed or given a new delivery meanwhile is left alone. content empties the
bodies and error texts, drops ctx.meta and its index tags, and marks the message's content
dropped; metadata and failed delete the rows. Returns how many messages and deliveries it
changed.
Parameters
channel
string
ids
readonly number[]
query
Returns
Promise<ExpireResult>
archiveRows()
archiveRows(
id):Promise<MessageRows|undefined>
Every column of one message's row and of each of its delivery rows, every generation, bodies
included: what an archive holds. undefined when the message is gone.
Parameters
id
number
Returns
Promise<MessageRows | undefined>
countExpiring()
countExpiring(
channel,query):Promise<ExpireResult>
How many messages, and their deliveries, query reaches on channel now.
Parameters
channel
string
query
Returns
Promise<ExpireResult>
listExpiring()
listExpiring(
channel,query,limit,after?):Promise<number[]>
Up to limit ids of the messages query reaches on channel, lowest first, above after.
Parameters
channel
string
query
limit
number
after?
number
Returns
Promise<number[]>
RetryingDelivery
A pending delivery that has failed at least once and waits to be tried again.
Example
const late = (d: RetryingDelivery): boolean => d.nextAttemptAt > Date.now() + 60_000;
Properties
attempts
readonlyattempts:number
destination
readonlydestination:number
id
readonlyid:number
messageId
readonlymessageId:number
nextAttemptAt
readonlynextAttemptAt:number
Epoch milliseconds of the next try.
SecretProvider
Where secrets come from: an environment variable, a file, or a secret store such as a cloud
provider's secrets manager. pathways.yaml names each provider under secrets: by a module whose
default export builds it, and a { secret: { provider, name, field, version } } reference anywhere
a secret is read asks it for one.
resolve is called once per secret when the engine starts, with a signal that aborts when the
provider's time limit passes or the start is cancelled. It rejects when the secret cannot be had,
preferably with a SecretError. Pathways reports a rejection by the reference's file and key,
the secret's name and the error's name and code, never the error's text, so an error may say
anything; a value must still never be put in one.
Example
const store: SecretProvider = {
async resolve(ref, signal) {
const found = await myStore.get(ref.name, { version: ref.version, signal });
return { value: found.text, version: found.version };
},
};
Extended by
Methods
resolve()
resolve(
ref,signal):Promise<ResolvedSecret>
Resolves one secret.
Parameters
ref
signal
AbortSignal
Returns
Promise<ResolvedSecret>
stop()?
optionalstop():void|Promise<void>
Releases what the provider holds open. Called once start has resolved every secret.
Returns
void | Promise<void>
SecretRef
One secret a provider is asked for: its name in the provider, and optionally one field of it and a version. A provider that has no fields or versions refuses a request that names one.
Example
const ref: SecretRef = { name: "lab/api", field: "token" };
Properties
field?
readonlyoptionalfield?:string
One field of a structured secret, or of a value that is a JSON object.
name
readonlyname:string
The secret's name in the provider: a variable, a path, a key. Never secret itself.
version?
readonlyoptionalversion?:string
A version the provider keeps. Undefined asks for the current one.
SendingMarker
The marker written on a delivery before it is handed to its destination: which attempt it is and when it started. The outcome write clears it, so a delivery found still marked was cut off between the send and the record of it, and the receiver may have it.
Example
await store.markSending(delivery.id, { attempt: delivery.attempts + 1, at: Date.now() });
Extended by
Properties
at
readonlyat:number
When it started, in epoch milliseconds.
attempt
readonlyattempt:number
The attempt's number, from 1.
SendStart
What MessageStore.markSending writes: the marker, and, when the delivery was found marked, the number of the attempt that was cut off.
Example
const start: SendStart = { attempt: 2, at: Date.now(), interruptedAttempt: 1 };
Extends
Properties
at
readonlyat:number
When it started, in epoch milliseconds.
Inherited from
attempt
readonlyattempt:number
The attempt's number, from 1.
Inherited from
interruptedAttempt?
readonlyoptionalinterruptedAttempt?:number
The attempt found cut off, kept on the delivery so every later send of it is flagged.
SourceConnector
Brings messages in. A source that polls (a folder, an inbox) implements poll, which the engine
calls on each pass its schedule makes due (the channel's poll:, or the engine-wide
pollInterval). A source that listens (MLLP, HTTP) implements start and stop. A custom
source's module builds one with a SourceFactory.
Example
const source: SourceConnector = {
async poll(sink, signal) {
for (const body of await takeWaiting({ signal })) await sink.receive(body);
},
};
Methods
poll()?
optionalpoll(sink,signal?):Promise<void>
One pass: takes in what is waiting, each message through sink. The engine never starts a pass
while the last one's call has not returned. At the pass's deadline (poll: { deadline }, 5
minutes by default) the engine stops waiting: signal aborts, which a source may pass to its
client to give up a hung listing or query, and sink.receive refuses every later message with
the code POLL_DEADLINE, so the source keeps it for the next pass.
Parameters
sink
signal?
AbortSignal
Returns
Promise<void>
start()?
optionalstart(sink):Promise<void>
Parameters
sink
Returns
Promise<void>
stop()?
optionalstop():Promise<void>
Returns
Promise<void>
SourceDownEvent
A source could not start listening (its port is taken, say), or a running source said it lost its
connection (MessageSink.sourceDown; then name is SourceDown). A listener is tried again after
retryInMs; a source that lost its connection is told to wait that long. Each is reported again
on each failure in a row, counting up attempt.
Example
if (event.type === "source-down" && event.attempt === 1) page(`${event.channel} cannot listen`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
attempt
readonlyattempt:number
How many tries have failed in a row, from 1.
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The error's code, such as EADDRINUSE, or the code the source passed, such as ECONNRESET.
name
readonlyname:string
The error's name, such as Error, or SourceDown when the source said it lost its connection.
retryAt
readonlyretryAt:string
When the next try is due, as an ISO 8601 time in UTC.
retryInMs
readonlyretryInMs:number
type
readonlytype:"source-down"
Overrides
SourceUpEvent
A source that was down is listening, or connected, again. A source that starts on the first try reports nothing.
Example
if (event.type === "source-up") resolveIncident(event.channel, "source");
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
type
readonlytype:"source-up"
Overrides
StoredContent
One row's content, exactly as storage holds it: a message's body and error, or a delivery's body and last error. With encryption on, these are the encrypted values.
Example
const row: StoredContent = { table: "messages", id: 1, body, error: null };
Properties
body
readonlybody:Uint8Array
error
readonlyerror:string|null
id
readonlyid:number
table
readonlytable:ContentTable
StoredDelivery
One delivery of a message, without its body.
Example
const waiting = (await store.deliveries(12)).filter((d: StoredDelivery) => d.status === "pending");
Properties
attempts
readonlyattempts:number
deliveredAt
readonlydeliveredAt:string|null
destination
readonlydestination:number
error
readonlyerror:string|null
The last failure, as stored: a stage and a code, such as delivery: ECONNREFUSED.
failedAt?
readonlyoptionalfailedAt?:string
When it failed for good (ISO 8601, UTC), by the store's clock. Unset until then, and on a delivery an older Pathways failed or a store that does not record it.
generation
readonlygeneration:number
id
readonlyid:number
interruptedAttempt?
readonlyoptionalinterruptedAttempt?:number
Set when an attempt of this delivery was cut off, its outcome never recorded, so the receiver may have it more than once: the number of the latest such attempt. Every later send of it is flagged as a possible duplicate.
nextAttemptAt
readonlynextAttemptAt:number
Epoch milliseconds before which it is not tried again; 0 when due at once.
seq
readonlyseq:number
Which output of the transform this is, from 0.
status
readonlystatus:DeliveryStatus
StoredDeliveryRow
Every column of one delivery's row, of any generation, as storage holds it, for an archive.
body is null once its message's content was dropped; its message id and channel are those of
the StoredMessageRow it comes with.
Example
const old = (row: StoredDeliveryRow, current: number): boolean => row.generation < current;
Properties
attempts
readonlyattempts:number
body
readonlybody:Uint8Array<ArrayBufferLike> |null
deliveredAt
readonlydeliveredAt:string|null
destination
readonlydestination:number
error
readonlyerror:string|null
Its last error text, as stored: can quote the message.
failedAt?
readonlyoptionalfailedAt?:string|null
When it failed for good; unset on a store that does not record it.
generation
readonlygeneration:number
id
readonlyid:number
interruptedAttempt
readonlyinterruptedAttempt:number|null
nextAttemptAt
readonlynextAttemptAt:number
sendingAt
readonlysendingAt:number|null
sendingAttempt
readonlysendingAttempt:number|null
seq
readonlyseq:number
status
readonlystatus:DeliveryStatus
StoredMessage
What storage holds about one message, without its body.
Example
const message: StoredMessage | undefined = await store.message(12);
Extends
Properties
channel
readonlychannel:string
Inherited from
contentDroppedAt?
readonlyoptionalcontentDroppedAt?:string
When the message's body and its deliveries' bodies were dropped, once its last delivery was
delivered (storage.content: dropAfterDelivery). Undefined while they are kept. Such a
message cannot be replayed or resent, and its content cannot be shown.
Inherited from
MessageSummary.contentDroppedAt
contentHash?
readonlyoptionalcontentHash?:string
The hash of the body as received, when one was stored with it: see StoreReceiveOptions.
Inherited from
deliveries
readonlydeliveries:DeliveryTally
The deliveries of the current generation, by status.
Inherited from
duplicateOf
readonlyduplicateOf:number|null
The latest earlier message with the same sender and control ID, when there is one.
Inherited from
error
readonlyerror:string|null
Why the message failed, as stored. The part after the second ": " is the error's own text,
which can quote the message: never print it.
generation
readonlygeneration:number
0 until the message is first replayed or resent; one more each time.
Inherited from
id
readonlyid:number
Inherited from
receivedAt
readonlyreceivedAt:string
Inherited from
size
readonlysize:number|null
The body's size in bytes, or null for a message stored before sizes were kept.
Inherited from
state
readonlystate:"delivered"|"received"|"filtered"|"failed"|"cleared"|"dry-run"|"queued"|"retrying"
The status and the current deliveries, as an operator sees them.
Inherited from
status
readonlystatus:MessageStatus
Inherited from
updatedAt
readonlyupdatedAt:string
When the message last changed: transformed, failed, replayed or cleared.
Inherited from
StoredMessageRow
Every column of one message's row, as storage holds it, for an archive: nothing a retention step
nulls or deletes is left out. body is null once the content was dropped; meta is the
metadata as stored (JSON, sealed under encryption), and a store wrapper reads it back.
Example
const mrnHeld = (row: StoredMessageRow): boolean => row.meta !== null;
Properties
body
readonlybody:Uint8Array<ArrayBufferLike> |null
channel
readonlychannel:string
contentDroppedAt
readonlycontentDroppedAt:string|null
contentHash
readonlycontentHash:string|null
dedupKey
readonlydedupKey:string|null
duplicateOf
readonlyduplicateOf:number|null
error
readonlyerror:string|null
Why it failed, as stored: can quote the message.
generation
readonlygeneration:number
id
readonlyid:number
idempotencyKey
readonlyidempotencyKey:string|null
meta
readonlymeta:string|null
receivedAt
readonlyreceivedAt:string
size
readonlysize:number|null
status
readonlystatus:MessageStatus
updatedAt
readonlyupdatedAt:string
StoredMeta
One message's metadata and its index tags, as stored: what pathways rekey reads to re-encrypt
the metadata and derive its tags again under the current key.
Example
const row: StoredMeta = { id: 12, channel: "adt", meta: storedJson, tags: [] };
Properties
channel
readonlychannel:string
id
readonlyid:number
meta
readonlymeta:string
tags
readonlytags: readonlyMetaTag[]
StoreReceiveOptions
What the engine tells storage about an incoming message. Every field is optional.
Example
await store.receive("adt", body, { duplicateKey: hashOfSenderAndControlId });
Properties
contentHash?
readonlyoptionalcontentHash?:string
A hash of the body as it arrived (the engine passes its SHA-256 in hex, for a channel that drops content after delivery), kept with the message's metadata after its body is dropped.
duplicateKey?
readonlyoptionalduplicateKey?:string
An opaque key for who sent the message and its control ID (the engine passes a hash). When an earlier message of the channel has the same key, this one is stored flagged as a possible duplicate of it.
failure?
readonlyoptionalfailure?:string
Stores the message as failed, with this reason, so it is kept but never transformed.
idempotencyKey?
readonlyoptionalidempotencyKey?:string
An opaque key that names this one message to its source (the engine passes a hash), stored
with the message. With resumed, when a message of the channel was already stored with the
same key, nothing is stored and that message's id comes back with alreadyStored: a source
that stored a message and then stopped before it could acknowledge it hands it in again on
restart, and it is stored once.
meta?
readonlyoptionalmeta?:Readonly<Record<string,string|number|boolean>>
The message's metadata. A store wrapper (encryption at rest) turns it into storedMeta and
metaTags before it reaches the store underneath; a store given it bare keeps it as JSON.
metaTags?
readonlyoptionalmetaTags?: readonlyMetaTag[]
The metadata's index tags, stored with the message in the same transaction. Set by a wrapper.
resumed?
readonlyoptionalresumed?:boolean
True when the message may have been stored already under idempotencyKey, by an earlier run:
only then is the key looked up.
size?
readonlyoptionalsize?:number
The body's size in bytes as it arrived; defaults to body.byteLength. A storage layer that
changes the body on its way in, such as encryption, passes the original size along.
storedMeta?
readonlyoptionalstoredMeta?:string
The metadata as the store keeps it: JSON, encrypted under encryption. Set by a wrapper.
TcpReply
The reply a tcp destination read for one output, for its reply check to judge.
Example
const text = (reply: TcpReply): string => Buffer.from(reply.body).toString("latin1");
Properties
body
readonlybody:Uint8Array
The reply's bytes, without the framing. Empty for an empty frame.
messageId
readonlymessageId:number
The id of the stored message the output came from.
seq
readonlyseq:number
Which output of that message's transform it was, from 0.
TcpReplyRequest
What a tcp source's reply builder is given: the id of the message just stored. It runs only once the message is stored.
Example
const request: TcpReplyRequest = { messageId: 42 };
Properties
messageId
readonlymessageId:number
The stored message's id.
TcpSourceFacts
What a tcp source knows about a message: the sender's address and, over mutual TLS, the common name in the subject of the certificate the sender presented.
Example
const peer = (source: TcpSourceFacts): string => source.tlsPeerSubject ?? "unknown";
Properties
kind
readonlykind:"tcp"
receivedAt
readonlyreceivedAt:string
When the message was stored, as an ISO 8601 time.
remoteAddress
readonlyremoteAddress:string|undefined
The sender's IP address, when the connection had one.
tlsPeerSubject
readonlytlsPeerSubject:string|undefined
The common name (CN) in the subject of the sender's TLS certificate, when it presented one.
TransformConfig
The configuration a transform reads as ctx.config: the config: block of pathways.yaml, with
the channel's own config: block over it key by key, and each { env: NAME } read from the
engine's environment when it starts. It is empty here: name your keys and their types by
declaration merging, once per project, and ctx.config is typed by them.
Example
declare module "@cosyte/pathways" {
interface TransformConfig {
readonly facility: string;
readonly labCodes: readonly string[];
}
}
Properties
facility?
readonlyoptionalfacility?:string
TransformContext
The second argument a transform receives: a frozen plain object, built for each message.
Nothing in it is logged, sent to a hook or shown on the health endpoint. A transform reads it; to change what is sent, it changes what it returns.
Example
import type { Transform } from "@cosyte/pathways";
export default ((msg, ctx) => {
if (ctx?.source?.kind === "mllp" && ctx.source.tlsPeerSubject !== "lab.example.test") return null;
return msg;
}) satisfies Transform;
Properties
attempt
readonlyattempt:number
Which run of the transform this is for the message: 1 the first time, and one more for each replay or resend before this one.
config
readonlyconfig:Readonly<TransformConfig>
The channel's configuration, resolved when the engine (or the command) started.
messageId
readonlymessageId:number
The stored message's id. 0 under pathways test, which stores nothing.
meta
readonlymeta:Readonly<Record<string,string|number|boolean>>
A per-message map of flat values. Always empty in this release: nothing sets it yet.
source
readonlysource:SourceFacts|undefined
What the source knew about the message when it took it in. Undefined when that is not known:
under pathways test, on a replay, and for a message taken in before the engine last started,
since the facts are kept in memory until the message is transformed and not stored; and for the
oldest of a channel's messages past the 4,096 whose facts the engine holds while they wait.
TransformedEvent
A message's transform returned one or more outputs, each now queued for every destination.
Example
if (event.type === "transformed") count("outputs", event.outputs);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
messageId
readonlymessageId:number
outputs
readonlyoutputs:number
How many outputs the transform returned.
type
readonlytype:"transformed"
Overrides
TransformFailedEvent
A message's transform threw, timed out or ran out of memory, or its input or output could not be read. The message is marked failed and the channel moves on.
Example
if (event.type === "transform-failed") page(`${event.channel}: ${event.name} at ${event.where ?? "?"}`);
Extends
Properties
at
readonlyat:string
When it happened, as an ISO 8601 time in UTC, such as 2026-11-07T12:00:00.000Z.
Inherited from
channel
readonlychannel:string
The channel's name.
Inherited from
code
readonlycode:string|undefined
The error's code, when it has one, such as a parser's NO_MSH_SEGMENT.
kind
readonlykind:TransformFailureKind
The stage that failed.
messageId
readonlymessageId:number
name
readonlyname:string
The error's name, such as TypeError.
type
readonlytype:"transform-failed"
Overrides
where
readonlywhere:string|undefined
Where in the transform it was thrown, as file:line:column, when it was.
WrappedKey
A data key, wrapped by a master key.
Example
const { keyId, wrapped }: WrappedKey = await keys.wrapKey(dataKey);
Properties
keyId
readonlykeyId:string
The id of the master key that wrapped it, stored beside the value so the value can still be read after another key becomes current. Printable ASCII with no space, 1 to 255 characters.
wrapped
readonlywrapped:Uint8Array
The data key, encrypted by that master key, in the provider's own layout: 1 to 65,535 bytes.
Type Aliases
AckBuilder
AckBuilder = (
request) =>Uint8Array|Promise<Uint8Array>
Builds the ACK an MLLP source sends, as the bytes of an HL7 v2 message (the source adds the MLLP
framing). The default is defaultAckBuilder; a custom one is the seam for a source whose
sender wants a different ACK, named in the channel's YAML as
source: { mllp: { ..., ackBuilder: ./ack.ts } } and exported as the module's default. If it
throws, or runs past the channel's transform time limit, the source logs the error's name and
sends the default ACK instead. It runs in the engine's own thread, once per message.
Parameters
request
Returns
Uint8Array | Promise<Uint8Array>
Example
// channels/adt/ack.ts: the default ACK, with MSH-3 set to what this sender expects.
import { defaultAckBuilder, type AckBuilder } from "@cosyte/pathways";
import { parseHL7 } from "@cosyte/hl7";
export default ((request) => {
const ack = parseHL7(Buffer.from(defaultAckBuilder(request)));
ack.setField("MSH.3", "PATHWAYS");
return Buffer.from(ack.toString());
}) satisfies AckBuilder;
AckReason
AckReason =
"stored"|"store-failed"|"unreadable"|"rejected"
Why an MLLP source answers as it does: stored (the message is durable or, on a channel that
stores nothing, every destination accepted it), store-failed (it could not be stored, and
onStoreFailure asked for an ACK), unreadable (it was stored, marked failed, because it has no
usable header or arrived damaged), or rejected (on a channel that stores nothing, the transform
failed or a destination rejected it, so sending it again will not help; answered AE).
Example
if (request.reason === "unreadable") console.warn(request.problem);
ArchiveErrorCode
ArchiveErrorCode =
"ERR_PATHWAYS_ARCHIVE_FORMAT"|"ERR_PATHWAYS_ARCHIVE_INTEGRITY"|"ERR_PATHWAYS_ARCHIVE_TOO_LARGE"
Why an archive could not be written or read:
ERR_PATHWAYS_ARCHIVE_FORMAT: it is not an archive in a layout this Pathways reads;ERR_PATHWAYS_ARCHIVE_INTEGRITY: its trailer does not match its records (count or HMAC), or a record names another channel than its header;ERR_PATHWAYS_ARCHIVE_TOO_LARGE: the file, its decompressed size or one record passes a limit of ArchiveLimits.
Decrypting it can also fail with an EncryptionError, ERR_PATHWAYS_UNKNOWN_KEY among them.
Example
const code: ArchiveErrorCode = "ERR_PATHWAYS_ARCHIVE_INTEGRITY";
AuditAction
AuditAction = typeof
AUDIT_ACTIONS[number]
One of AUDIT_ACTIONS.
Example
const action: AuditAction = "content-view";
AuditDetail
AuditDetail =
Readonly<Record<string,string|number|boolean>>
An audit entry's particulars: counts, statuses, codes and hashes. Never content.
Example
const detail: AuditDetail = { status: "transformed", queued: 2 };
AuditEventAction
AuditEventAction =
"search"|"view"|"content-view"|"replay"|"resend"|"clear"|"retention"|"config"|"lease-release"|"migrate"|"channel-pause"|"channel-resume"|"channel-stop"|"channel-halt"|"channel-reload"|"verify"|"bulk-replay"|"archive"|"node-forget"
Every action the audit log records, as an AuditEvent's action: search (messages
listed), view (one message's details, without content), content-view, replay, resend,
clear, retention (a run, with what it deleted), config (a configuration change seen when
the engine started), lease-release (the engine lease freed by force), migrate (schema steps
applied by pathways migrate), channel-pause, channel-resume, channel-stop,
channel-halt and channel-reload (a pathways channel command), verify (a stored
message read by pathways verify), bulk-replay (a run of pathways replay --status failed:
its scope, each batch and a summary), archive (a sealed archive retention wrote and checked
before changing what it holds), and node-forget (a stale node's row removed by
pathways nodes forget). Kept equal to the audit log's own list by a test.
Example
const sensitive = (action: AuditEventAction): boolean => action === "content-view";
AuditListener
AuditListener = (
entry) =>void|Promise<void>
Told of each audit entry once it is stored. A lifecycle hook subscribes one to forward the audit log, to a file or a SIEM. It may be async; the action that wrote the entry waits for it to settle, and a listener that throws or rejects never undoes the action or the entry.
Parameters
entry
Returns
void | Promise<void>
Example
const forward: AuditListener = async (entry) => {
await siem.send({ at: entry.at, actor: entry.actor, action: entry.action });
};
AuditOutcome
AuditOutcome =
"ok"|"refused"|"not-found"
How an audited action ended: done, refused (its detail.reason says why), or no such message.
Example
const failed = (outcome: AuditOutcome): boolean => outcome !== "ok";
CcdaDocument
CcdaDocument =
Ccda.CcdaDocument
A C-CDA document as a ccda channel's transform receives it: @cosyte/ccda's CcdaDocument.
Pathways exports the type only; import the class from @cosyte/ccda to build one.
Example
import type { CcdaDocument, Transform } from "@cosyte/pathways";
export default ((document) => (document.getPatient() ? document : null)) satisfies Transform<CcdaDocument>;
ChannelRunState
ChannelRunState =
"running"|"paused"|"stopped"
What an operator set a channel to: running (the default), paused (its source takes nothing
in, while stored messages go on through the transform and delivery) or stopped (nothing of it
runs). Kept in storage, so a restart, and every engine on the same database, honours it.
Example
const state: ChannelRunState = "paused";
ChannelStateStore
ChannelStateStore =
Required<Pick<MessageStore,"channelStates"|"setChannelState">>
The channel state methods of MessageStore, both present.
Example
const states: ChannelStateStore | undefined = channelStateStore(store);
ClearResult
ClearResult = {
deliveries:number;done:true;entry:AuditEntry;messages:number; } | {done:false;entry:AuditEntry;reason:Extract<RefusalReason,"not-found"|"not-failed">; }
What a clear did, with the audit entry written in the same transaction.
Union Members
Type Literal
{ deliveries: number; done: true; entry: AuditEntry; messages: number; }
deliveries
readonlydeliveries:number
Its deliveries that had failed for good.
done
readonlydone:true
entry
readonlyentry:AuditEntry
messages
readonlymessages:number
1 when the message itself had failed, else 0.
Type Literal
{ done: false; entry: AuditEntry; reason: Extract<RefusalReason, "not-found" | "not-failed">; }
Example
const result: ClearResult = await store.clear(12, "noah");
if (result.done) console.log(result.messages, result.deliveries);
ContentTable
ContentTable =
"messages"|"deliveries"
Which table a StoredContent row is from.
Example
const table: ContentTable = "deliveries";
CsvRecord
CsvRecord =
Record<string,string>
One row of a csv table read with a header, or of a fixed-width one: its cells by column name, on
an object with no prototype, so a column named __proto__ or constructor is a cell like any
other.
Example
const row: CsvRecord = Object.assign(Object.create(null), { code: "TEST-001" });
CsvTable
CsvTable = {
header:string[];rows:CsvRecord[]; } | {header:null;rows:string[][]; }
What a csv channel's transform receives, and may return: the header and the rows. With a header
(and in fixed-width mode, where the header is the column names), each row is a CsvRecord;
without one, header is null and each row is an array of cells. A transform may change it in
place or build a new one; rows it leaves untouched are written back byte for byte.
Example
const table: CsvTable = { header: null, rows: [["TEST-001", "Example"]] };
DeliveryCounts
DeliveryCounts =
Readonly<Record<Exclude<DeliveryStatus,"dry-run">,number>> &object
How many deliveries stand at each status. dry-run is there only when some delivery ran dry, so
the counts of a project that never runs a destination dry read as they did before dry runs.
Type Declaration
dry-run?
readonlyoptionaldry-run?:number
Example
const recorded = (counts: DeliveryCounts): number => counts.delivered + (counts["dry-run"] ?? 0);
DeliveryStatus
DeliveryStatus =
"pending"|"delivered"|"failed"|"cleared"|"dry-run"
Where one delivery of a message to one destination stands. cleared: it failed for good, and an
operator cleared it, or replayed its message. dry-run: its destination runs dry (dryRun: true,
or pathways start --dry-run), so its output was recorded and never sent.
Example
const open = (status: DeliveryStatus): boolean => status === "pending";
DeliveryTally
DeliveryTally =
DeliveryCounts&object
How many of one message's deliveries stand at each status. retrying counts the pending ones
that have failed at least once, so it is part of pending. dry-run is there only when one ran
dry.
Type Declaration
retrying
readonlyretrying:number
Example
const open = (tally: DeliveryTally): number => tally.pending + tally.failed;
DestinationFactory
DestinationFactory<
Options> = (options,context) =>DestinationConnector|Promise<DestinationConnector>
Builds a custom destination: a module's default export, named in a channel's YAML as
destinations: [{ module: ./my-destination.ts, options: {...} }]. Its deliver throws to have the
engine retry, or throws a DeliveryRejected when sending again cannot help.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
DestinationConnector | Promise<DestinationConnector>
Example
// channels/inbox/my-destination.ts
import { DeliveryRejected, type DestinationFactory } from "@cosyte/pathways";
export default ((options) => ({
async deliver(message) {
const status = await putObject(options.bucket, `${message.messageId}-${message.seq}`, message.body);
if (status === 400) throw new DeliveryRejected("HTTP 400");
if (status !== 200) throw Object.assign(new Error("not stored"), { code: `HTTP_${status}` });
},
})) satisfies DestinationFactory<{ bucket: string }>;
EncryptionErrorCode
EncryptionErrorCode =
"ERR_PATHWAYS_NOT_ENCRYPTED"|"ERR_PATHWAYS_ENCRYPTED"|"ERR_PATHWAYS_MALFORMED"|"ERR_PATHWAYS_FORMAT_VERSION"|"ERR_PATHWAYS_UNKNOWN_KEY"|"ERR_PATHWAYS_INTEGRITY"|"ERR_PATHWAYS_KEY_PROVIDER"
Why an encrypted value could not be written or read:
ERR_PATHWAYS_NOT_ENCRYPTED: the value is not in the encrypted format: it was stored while encryption wasnone, or its first bytes were changed;ERR_PATHWAYS_ENCRYPTED: the value is encrypted, and encryption isnone;ERR_PATHWAYS_MALFORMED: the value starts like an encrypted one, but its layout is broken;ERR_PATHWAYS_FORMAT_VERSION: the value is in a format version this Pathways does not read;ERR_PATHWAYS_UNKNOWN_KEY: the master key the value names is not configured;ERR_PATHWAYS_INTEGRITY: the value, or its wrapped data key, failed its integrity check: it was changed or damaged, or the key configured under its id is not the key that wrote it;ERR_PATHWAYS_KEY_PROVIDER: a key provider returned a key id or a key that cannot be used.
Example
const code: EncryptionErrorCode = "ERR_PATHWAYS_INTEGRITY";
ExtensionOptions
ExtensionOptions =
Readonly<Record<string,unknown>>
The options: an extension's YAML block gives it, as parsed: plain YAML values, not checked by
Pathways. A factory names its own shape as the type parameter and should check what it reads.
Credentials never go in options; read them from context.env.
Example
const folder = typeof options["folder"] === "string" ? options["folder"] : "./in";
FakeSecretEntry
FakeSecretEntry =
string|FakeSecret| readonlyFakeSecret[]
What a fake holds for one name: a value, a version, or every version with the current one last.
FhirResource
FhirResource =
Fhir.FhirComplex
A FHIR R4 resource as a fhir channel's transform receives it: @cosyte/fhir's FhirComplex, an
immutable model that keeps every decimal exactly as written. Read it with @cosyte/fhir's
helpers (getProperty, resourceType, readSafety), and return it, a new model, or a string.
Example
import type { FhirResource, Transform } from "@cosyte/pathways";
export default ((resource) => resource) satisfies Transform<FhirResource>;
Format
Format = typeof
FORMATS[number]
A channel's message format, and so what its transform receives:
hl7v2: parsed with@cosyte/hl7(the default);fhir: FHIR R4 JSON, parsed with@cosyte/fhir;x12: an X12 interchange, parsed with@cosyte/x12;ccda: a C-CDA document, parsed with@cosyte/ccda;json: any JSON, asJSON.parsereads it;text: the body as a string, unparsed;csv: delimited or fixed-width lines, as a table of rows.
Example
const format: Format = "x12";
Hl7Message
Hl7Message =
Hl7.Hl7Message
An HL7 v2 message as an hl7v2 channel's transform receives it: @cosyte/hl7's Hl7Message.
Pathways exports the type only; import the class from @cosyte/hl7 to build one.
Example
import type { Hl7Message, Transform } from "@cosyte/pathways";
export default ((msg) => (msg.is("ORU^R01") ? msg : null)) satisfies Transform<Hl7Message>;
HookFactory
HookFactory<
Options> = (options,context) =>LifecycleHook|Promise<LifecycleHook>
Builds a lifecycle hook: a module's default export, named in pathways.yaml (engine-wide) or a
channel's YAML (that channel only) as hooks: [./hooks/pager.ts] or
hooks: [{ module: ./hooks/pager.ts, options: {...}, events: [...], timeout: 2s }].
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
LifecycleHook | Promise<LifecycleHook>
Example
// hooks/pager.ts
import type { HookFactory } from "@cosyte/pathways";
export default ((_options, context) => ({
async onEvent(event, signal) {
if (event.type !== "delivery-rejected" && event.type !== "source-down") return;
await page(context.env["PAGER_KEY"], `${event.channel}: ${event.type}`, signal);
},
})) satisfies HookFactory;
JsonValue
JsonValue =
string|number|boolean|null|JsonValue[] | {[key:string]:JsonValue; }
Any JSON value, as a json channel's transform receives it from JSON.parse.
Example
const value: JsonValue = { status: "ok", tries: [1, 2] };
KeyProviderFactory
KeyProviderFactory<
Options> = (options,context) =>KeyProvider|Promise<KeyProvider>
Builds a custom key provider for encryption at rest: a module's default export, named in
pathways.yaml as encryption: { provider: { module: ./kms.ts, options: {...} } }. It returns a
KeyProvider (or a promise of one) that wraps each data key under its current master key and
unwraps keys under its older ones, such as one over a cloud key service. It is built once, when
the engine (or pathways rekey) starts.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
KeyProvider | Promise<KeyProvider>
Example
// kms.ts
import type { KeyProviderFactory } from "@cosyte/pathways";
export default (async (options, context) => {
const kms = await connectKms(options.region, context.env["KMS_CREDENTIALS"]);
return {
currentKeyId: options.keyId,
wrapKey: async (dataKey) => ({ keyId: options.keyId, wrapped: await kms.encrypt(options.keyId, dataKey) }),
unwrapKey: (keyId, wrapped) => kms.decrypt(keyId, wrapped),
};
}) satisfies KeyProviderFactory<{ region: string; keyId: string }>;
LeaseClaim
LeaseClaim = {
acquired:true;epoch:number; } | {acquired:false;holder:LeaseHolder; }
What MessageStore.acquireLease answers: the lease and its new epoch, or the holder that keeps it.
Example
const claim = await store.acquireLease(ENGINE_LEASE, claimant, 15_000);
if (!claim.acquired) console.error(claim.holder.node);
LeaseStore
LeaseStore =
Required<Pick<MessageStore,"acquireLease"|"renewLease"|"releaseLease"|"leaseHolder">>
The lease methods of MessageStore, all four present.
Example
const leases: LeaseStore | undefined = leaseStore(store);
LifecycleEvent
LifecycleEvent =
ReceivedEvent|ReceiveFailedEvent|TransformedEvent|FilteredEvent|TransformFailedEvent|DeliveredEvent|DeliveryRetryEvent|DeliveryRedirectedEvent|DeliveryRejectedEvent|DeliveryExhaustedEvent|SourceDownEvent|SourceUpEvent|AuditEvent
Any lifecycle event: switch on type to narrow it. Engine-wide hooks receive every channel's
events and the audit log's; a channel's own hooks receive that channel's events.
Example
function describe(event: LifecycleEvent): string {
switch (event.type) {
case "delivery-rejected":
return `delivery ${event.deliveryId} rejected (${event.reason})`;
case "source-down":
return `source down, try ${event.attempt}`;
default:
return event.type;
}
}
LifecycleEventType
LifecycleEventType = typeof
LIFECYCLE_EVENTS[number]
The name of one lifecycle event: one of LIFECYCLE_EVENTS.
Example
const paged: readonly LifecycleEventType[] = ["delivery-rejected", "transform-failed"];
LogValue
LogValue =
string|number|boolean|null|undefined
A value a log line may carry. Objects are left out on purpose: an error or a message object can carry content.
Example
const value: LogValue = 42;
MessageFor
MessageFor<
F> =MessageTypes[F]
The message a channel of format F hands its transform.
Type Parameters
F
F extends Format
Example
type Document = MessageFor<"ccda">; // CcdaDocument
MessageMeta
MessageMeta =
Readonly<Record<string,string|number|boolean>>
A message's metadata: flat values by key, as ctx.meta holds them. Stored encrypted with the
message; the keys a channel's index: block names are also stored as truncated index tags, so a
search can find them without decrypting every row.
Example
const meta: MessageMeta = { mrn: "SYN-000123", visit: 2 };
MessageState
MessageState = typeof
MESSAGE_STATES[number]
A message's state as an operator sees it: its status, and for a transformed one, its deliveries.
Example
const waiting: readonly MessageState[] = ["received", "queued", "retrying"];
MessageStatus
MessageStatus =
"received"|"transformed"|"filtered"|"failed"|"cleared"
Where a stored message stands. cleared: it failed, and an operator cleared it with
pathways clear.
Example
const done = (status: MessageStatus): boolean => status === "transformed" || status === "filtered";
MessageTypes
MessageTypes =
{ [F in Format]: FormatModel<F> }
The message each format hands a transform, by the format a channel declares, read from the
format registry: hl7v2 an Hl7Message, fhir a FhirResource, x12 an
X12Interchange, ccda a CcdaDocument, json a JsonValue (a string a
json transform returns is sent as it is, not JSON-quoted), text the body as a string, and
csv a CsvTable.
Example
type Claim = MessageTypes["x12"]; // X12Interchange
NodeJoin
NodeJoin = {
joined:true; } | {holder:NodeRow;joined:false; }
What MessageStore.joinNode answers: joined, or the live row that holds the id.
Example
const join = await store.joinNode(info);
if (!join.joined) console.error(join.holder.host);
NodeRole
NodeRole = typeof
NODE_ROLES[number]
One of NODE_ROLES.
Example
const role: NodeRole = "active";
NodeStore
NodeStore =
Required<Pick<MessageStore,"joinNode"|"nodeHeartbeat"|"leaveNode"|"nodes"|"forgetNode">>
The node methods of MessageStore, all five present.
Example
const nodes: NodeStore | undefined = nodeStore(store);
RefusalReason
RefusalReason =
"not-found"|"changed"|"pending"|"not-failed"|"content-dropped"
Why storage did not record a replay or a clear. content-dropped: the message's content was
dropped after delivery, so there is nothing to replay or resend.
Example
const retryable = (reason: RefusalReason): boolean => reason === "changed" || reason === "pending";
RequeueResult
RequeueResult = {
done:true;entry:AuditEntry;generation:number;queued:number;status:MessageStatus;superseded:number; } | {done:false;entry:AuditEntry;reason:Extract<RefusalReason,"not-found"|"changed"|"pending"|"content-dropped">; }
What a replay or a resend did, with the audit entry written in the same transaction.
Union Members
Type Literal
{ done: true; entry: AuditEntry; generation: number; queued: number; status: MessageStatus; superseded: number; }
done
readonlydone:true
entry
readonlyentry:AuditEntry
generation
readonlygeneration:number
queued
readonlyqueued:number
Deliveries queued, at the back of each destination's queue.
status
readonlystatus:MessageStatus
superseded
readonlysuperseded:number
Earlier deliveries that had failed for good, now cleared.
Type Literal
{ done: false; entry: AuditEntry; reason: Extract<RefusalReason, "not-found" | "changed" | "pending" | "content-dropped">; }
Example
const result: RequeueResult = await store.requeue(12, request, "noah");
if (!result.done) console.log(result.reason);
RetentionStep
RetentionStep =
"content"|"metadata"|"failed"
One step of retention, each with its own clock:
content: a done message's bodies, error texts andctx.metago, and its row stays;metadata: a done message's row goes, with its deliveries;failed: a failed message's row goes, with its deliveries.
Example
const step: RetentionStep = "content";
SecretErrorCode
SecretErrorCode =
"ERR_PATHWAYS_SECRET_NOT_FOUND"|"ERR_PATHWAYS_SECRET_FIELD"|"ERR_PATHWAYS_SECRET_VERSION"|"ERR_PATHWAYS_SECRET_UNREADABLE"
The codes a SecretError carries:
ERR_PATHWAYS_SECRET_NOT_FOUND: the provider has no secret by that name;ERR_PATHWAYS_SECRET_FIELD: the secret has no such field, or holds no fields;ERR_PATHWAYS_SECRET_VERSION: the provider has no such version, or keeps none;ERR_PATHWAYS_SECRET_UNREADABLE: the provider could not read it.
Example
const code: SecretErrorCode = "ERR_PATHWAYS_SECRET_NOT_FOUND";
SecretProviderFactory
SecretProviderFactory<
Options> = (options,context) =>SecretProvider|Promise<SecretProvider>
Builds a secret provider: a module's default export, named in pathways.yaml as
secrets: { vault: { module: ./vault.ts, options: {...} } }. It returns a SecretProvider (or a
promise of one) that { secret: { provider: vault, name } } references are resolved through. It
is built when the engine (or a command that opens the store) starts, asked for each secret, and
stopped. Building it gets the provider's timeout, as each request does: a factory still waiting
then fails the start. Its own credentials come from context.env, or from a file its options name.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
SecretProvider | Promise<SecretProvider>
Example
// vault.ts
import type { SecretProviderFactory } from "@cosyte/pathways";
export default (async (options, context) => {
const client = await connectStore(options.address, context.env["STORE_ROLE_TOKEN"]);
return {
resolve: async (ref, signal) => {
const found = await client.read(ref.name, { field: ref.field, version: ref.version, signal });
return { value: found.value, version: found.version };
},
stop: () => client.close(),
};
}) satisfies SecretProviderFactory<{ address: string }>;
SourceAckCode
SourceAckCode =
"AA"|"AE"|"AR"
The ACK codes an MLLP source answers with.
Example
const positive = (code: SourceAckCode): boolean => code === "AA";
SourceFactory
SourceFactory<
Options> = (options,context) =>SourceConnector|Promise<SourceConnector>
Builds a custom source: a module's default export, named in a channel's YAML as
source: { module: ./my-source.ts, options: {...} }. It may return the connector or a promise of
it. The engine calls poll on every pass, or start once and stop at shutdown.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
SourceConnector | Promise<SourceConnector>
Example
// channels/inbox/my-source.ts
import type { SourceFactory } from "@cosyte/pathways";
export default ((options, context) => ({
async poll(sink) {
for (const body of await fetchWaiting(options.queue, context.env["QUEUE_TOKEN"])) {
await sink.receive(body); // stored before it resolves: acknowledge only after
await acknowledge(body);
}
},
})) satisfies SourceFactory<{ queue: string }>;
SourceFacts
SourceFacts =
FileSourceFacts|HttpSourceFacts|MllpSourceFacts|TcpSourceFacts|ModuleSourceFacts
What the source knew about a message when it took it in, by the kind of source.
Example
const from = (source: SourceFacts): string | undefined =>
source.kind === "http" || source.kind === "mllp" ? source.remoteAddress : undefined;
StoreFactory
StoreFactory<
Options> = (options,context) =>MessageStore|Promise<MessageStore>
Builds a custom storage adapter: a module's default export, named in pathways.yaml as
storage: { module: ./my-store.ts, options: {...} }. It returns a MessageStore, ready to
use (its schema created or migrated), or a promise of one. The engine calls close at shutdown.
Type Parameters
Options
Options = ExtensionOptions
Parameters
options
Options
context
Returns
MessageStore | Promise<MessageStore>
Example
// stores/my-store.ts
import type { MessageStore, StoreFactory } from "@cosyte/pathways";
export default (async (options, context) => {
const pool = await connect(options.url, context.env["STORE_PASSWORD"]);
await migrate(pool);
return new PoolStore(pool) satisfies MessageStore;
}) satisfies StoreFactory<{ url: string }>;
TcpReplyBuilder
TcpReplyBuilder = (
request) =>Uint8Array|string|Promise<Uint8Array|string>
Builds the reply a tcp source sends once a message is stored, named in the channel's YAML as
source: { tcp: { ..., reply: { builder: ./reply.ts } } } and exported as the module's default.
It returns the reply's bytes, or a string sent as UTF-8; the source adds the framing. It runs in
the engine's own thread, once per message, under the channel's transform time limit. If it throws,
runs past the limit or returns something else, the source logs tcp-reply-failed and closes the
connection without a reply: the message is stored, and a sender that sends it again stores it twice.
Parameters
request
Returns
Uint8Array | string | Promise<Uint8Array | string>
Example
// channels/orders-in/reply.ts: answers OK and the stored id, one line.
import type { TcpReplyBuilder } from "@cosyte/pathways";
export default (({ messageId }) => `OK ${messageId}`) satisfies TcpReplyBuilder;
TcpReplyCheck
TcpReplyCheck = (
reply) =>TcpReplyVerdict|Promise<TcpReplyVerdict>
Judges the reply a tcp destination with expectReply: true read, named in the channel's YAML as
replyCheck: ./check.ts and exported as the module's default. Without one, any reply means
delivered. If it throws, runs past the channel's transform time limit or returns anything but a
TcpReplyVerdict, the delivery is retried with the code REPLY_CHECK_FAILED.
Parameters
reply
Returns
TcpReplyVerdict | Promise<TcpReplyVerdict>
Example
// channels/orders-out/check.ts: the receiver answers ACK or NAK.
import type { TcpReplyCheck } from "@cosyte/pathways";
export default ((reply) => {
const text = Buffer.from(reply.body).toString("latin1");
return text === "ACK" ? "delivered" : text === "NAK" ? "rejected" : "retry";
}) satisfies TcpReplyCheck;
TcpReplyVerdict
TcpReplyVerdict =
"delivered"|"retry"|"rejected"
What a reply check decides: delivered, retry (the engine tries the same output again, holding
later ones behind it), or rejected (failed for good; the destination moves on).
Example
const verdict: TcpReplyVerdict = "retry";
Transform
Transform<
In,Out> = (message,ctx?) =>TransformOutput<Out> |Promise<TransformOutput<Out>>
A channel's transform: the default export of its transform.ts. It receives the parsed message
(an Hl7Message from @cosyte/hl7 unless you name another type) and returns what to send. It
may be async.
Its second argument, ctx, is a TransformContext: the message's id, which run this is,
what its source knew, and the channel's configuration. The engine always passes it; it is optional
in the type, so a transform that takes one argument is a Transform too, and a test can call one
with the message alone.
Out is what it returns, when that differs from what it receives: a transform from HL7 v2 to FHIR
is a Transform<Hl7Message, FhirResource>. A model from any of the four libraries is written by
its own library, and a string is sent as it is.
Type Parameters
In
In = Hl7Message
Out
Out = In
Parameters
message
In
ctx?
Returns
TransformOutput<Out> | Promise<TransformOutput<Out>>
Example
import type { Transform } from "@cosyte/pathways";
export default ((msg) => {
if (!msg.is("ADT^A01")) return null;
msg.setField("MSH.5", "LAB");
return msg;
}) satisfies Transform;
TransformFailureKind
TransformFailureKind =
"load"|"parse"|"transform"|"output"|"timeout"|"memory"|"crash"
The stage a message's transform failed in: load (the transform would not load), parse (the
message could not be read in the channel's format), transform (the transform threw), output
(what it returned could not be written), timeout, memory or crash (its worker died). Kept
equal to the worker's own list by a test.
Example
const yourFix = (kind: TransformFailureKind): boolean => kind === "transform" || kind === "output";
TransformFor
TransformFor<
F,To> =Transform<MessageFor<F>,MessageFor<To>>
A transform for a channel of format F, typed from the format name in channel.yaml. Name a
second format, To, when the transform returns another format's model.
Type Parameters
F
F extends Format
To
To extends Format = F
Example
import type { TransformFor } from "@cosyte/pathways";
// channel.yaml: format: x12
export default ((interchange) =>
interchange.groups.some((group) => group.gs.elements[1] === "HC") ? interchange : null
) satisfies TransformFor<"x12">;
TransformOutput
TransformOutput<
M> =M|string|Uint8Array| readonly (M|string|Uint8Array)[] |null|undefined
What a transform may return: a message, a string or bytes to send on, several of them, or null
(or nothing) to filter the message out. A string is sent as UTF-8; bytes (a Uint8Array, which a
Buffer is) are sent as they are, so a body in another charset keeps its bytes.
Type Parameters
M
M
Example
const filtered: TransformOutput<string> = null;
const bytes: TransformOutput<string> = Buffer.from("MSH|^~\\&|LAB", "latin1");
X12Interchange
X12Interchange =
X12.X12Interchange
An X12 interchange as an x12 channel's transform receives it: @cosyte/x12's
X12Interchange, whose typed readers (get835, get837Claims and the rest) take it from there.
Example
import type { Transform, X12Interchange } from "@cosyte/pathways";
export default ((interchange) => interchange) satisfies Transform<X12Interchange>;
Variables
ENGINE_LEASE
constENGINE_LEASE:"engine"="engine"
The name of the lease an engine takes on its database before it opens a source, so a second engine on the same database is refused. The fenced writes check this lease's epoch.
Example
const holder = await store.leaseHolder?.(ENGINE_LEASE);
LIFECYCLE_EVENTS
constLIFECYCLE_EVENTS: readonly ["received","receive-failed","transformed","filtered","transform-failed","delivered","delivery-retry","delivery-redirected","delivery-rejected","delivery-exhausted","source-down","source-up","audit"]
Every lifecycle event's type, in the order a message meets them. A hook entry's events: list in
the YAML names some of these to receive only those.
Example
LIFECYCLE_EVENTS.includes("delivered"); // true
NODE_ROLES
constNODE_ROLES: readonly ["single","standby","active","proving","draining","fenced"]
Every role a node row can hold:
single: an engine on a store without leases, the only one.standby: waiting for the engine lease another node holds.active: holding the engine lease, or about to take it.proving: reserved for a node that checks it can take over before it does; nothing sets it yet.draining: told to stop, and closing its sources.fenced: it lost the engine lease and is stopping.
Example
NODE_ROLES.includes("standby"); // true
VERSION
constVERSION:"0.0.3"="0.0.3"
The version of @cosyte/pathways, kept equal to package.json#version (a test fails when they
differ).
Example
import { VERSION } from "@cosyte/pathways";
console.log(VERSION);
Functions
defaultAckBuilder()
defaultAckBuilder(
request):Uint8Array
Builds an MLLP source's ACK with @cosyte/hl7's buildAck: sender and receiver swapped, MSA-2
echoing the inbound MSH-10, and, for a negative ACK, one ERR segment with an HL7 table 0357
condition code (207 when storing failed or a channel that stores nothing could not pass the
message on, 101 for a missing MSH-10, 100 otherwise). It never puts
message content in the ERR segment. An inbound header that declares an ISO 8859 character set in
MSH-18 is answered in Latin-1, so MSA-2 echoes the control ID byte for byte; anything else is
answered in UTF-8.
Parameters
request
Returns
Uint8Array
Example
const ack = defaultAckBuilder({
code: "AA",
reason: "stored",
problem: undefined,
header: parseHL7(mshSegment),
messageId: 1,
});
fakeSecretProvider()
fakeSecretProvider(
secrets?):FakeSecretProvider
A provider for tests, holding secrets by name. It keeps versions, takes field from a value that
is a JSON object as the built-in providers do, honours the abort signal, and rejects a missing
name, field or version with a SecretError.
Parameters
secrets?
Readonly<Record<string, FakeSecretEntry>> = {}
Returns
Example
// test/fake-vault.ts, named in a test project's pathways.yaml as secrets: { vault: { module: ./fake-vault.ts } }
export default () => fakeSecretProvider({ "lab/api": "synthetic-token-0123456789" });