@frontera-sdk/functions
v1.50.19
Published
Author Frontera functions: manifest, triggers and the typed handler contract.
Readme
@frontera-sdk/automation
Author a Frontera automation: a named, triggered handler the platform runs on
your behalf, deployed with frontera automation deploy.
bun add @frontera-sdk/automationimport { automation } from '@frontera-sdk/automation'
export default automation(
{
name: 'late-shipment-digest',
trigger: { cron: '0 8 * * 1-5' },
grants: ['blueprint:read'],
},
async (ctx) => {
const late = await ctx.blueprint.query('Shipment', {
where: { property: 'deliveryStatus', op: 'eq', value: 'late' },
})
await ctx.log(`${late.rows.length} late`, { hasMore: late.hasMore })
},
)A trigger is { cron } or { manual: true }, never both — the type refuses
the ambiguous shape rather than leaving the runner to decide what it means.
ctx.log never rejects: telemetry must not be able to fail a run.
Steps
ctx.step.run(name, fn) makes a piece of work durable. The platform stores the
result, and if the run is retried the step is not run again — it returns what it
returned the first time.
async (ctx) => {
const overdue = await ctx.step.run('load-overdue', () =>
ctx.blueprint.query('Invoice', {
where: { property: 'status', op: 'eq', value: 'overdue' },
}),
)
for (const [i, row] of overdue.rows.entries()) {
await ctx.step.run(`notify:${i}`, () =>
ctx.http.fetch({ url: 'https://hooks.example.com/notify', method: 'POST',
body: JSON.stringify({ invoiceId: row.id }) }),
)
}
return { notified: overdue.rows.length }
}Three rules, and each one is a real failure mode rather than a style note:
- Names are unique within a run. The platform memoizes by name, so reusing one would hand back the first step's result. It is refused instead, naming the collision — inside a loop, put the index in the name.
- Results are JSON. A step's result is stored and replayed, so a
Datecomes back as a string. Return data, not objects with behaviour. - Code outside a step re-runs. After each step the handler restarts from the
top, with completed steps returning their stored results. A
ctx.httpcall that is not inside a step therefore fires once per step and spends its call budget every time.
Each step is recorded on the run, and every ctx call made inside one records
the step it belongs to — so a run's trail is a tree of named work rather than a
flat list.
The Console reads your deployed code and draws the whole shape: every step, both
arms of every if with the condition on the edge, and a loop as a single node.
A step named with a template shows as its pattern (items:${i}), because one
loop in the code is one step in the picture however many times it runs. Each
card names what the step reaches — Blueprint, Agent, HTTP — read from the ctx
calls in its body, and clicking one shows that step's source.
automation() freezes the manifest it returns, including a copy of the
trigger — a later mutation of the object you passed in cannot silently change
the schedule the runner registers.
validateManifest is the same check the CLI runs before deploy, exported so
you can run it in your own tests: names are kebab-case segments, and a cron
expression is parsed rather than pattern-matched.
Testing a handler
createTestContext gives you a ctx to call your handler with, so a branch can
be exercised without deploying and waiting for a day with the right data.
import { createTestContext } from '@frontera-sdk/automation'
import handler from './index'
const empty = createTestContext({ grants: ['blueprint:read'] })
expect(await handler.handler(empty.ctx)).toEqual({ handled: 0 })
expect(empty.steps).toEqual(['load', 'nothing-to-do'])
const busy = createTestContext({
grants: ['blueprint:read', 'agent:ava:run'],
blueprint: { Invoice: { rows: [{ id: 'INV-1' }], hasMore: false } },
agents: { ava: () => ({ text: 'looks fine' }) },
})
await handler.handler(busy.ctx)
expect(busy.calls.map((c) => c.kind)).toContain('agent')It refuses what the platform refuses, in the same words: a grant your manifest
does not declare, and a step name used twice. An agent or an HTTP call you did
not stub throws rather than answering — a fabricated 200 or an empty agent
reply is a test that passes while asserting nothing. An object type you did not
stub returns no rows, because that is a real answer and usually the branch worth
testing.
What it does not simulate is resumption: in production your handler is re-entered after every step, and here it runs once, straight through.
License
Apache-2.0. See LICENSE.
