Browse the docs

Examples

Approvals example

A team approvals tool. Workspace members as approvers, email on submit and decision, and a weekday digest schedule.

bunx @revnu/fuast new approvals --template approvals

Anyone in the workspace submits a request with an amount and a reason. An approver approves or rejects it with an optional note. Both sides get email, and a weekday digest reminds approvers of anything still pending.

What it shows

  • auth: { requireSignIn: true } for a workspace tool.
  • Approvers from ctx.workspace.members(), optionally narrowed by APPROVER_EMAILS in env.
  • submit emails approvers. decide checks that the caller is an approver, is not the submitter, and that the request is still pending, then emails the submitter. Email is always the last step.
  • A digest schedule at 0 9 * * 1-5 (09:00 UTC on weekdays), plus sendDigestNow, which runs the same helper on demand for testing.
  • WebMCP tools on submit, decide, mine, and queue, so a browser agent can file and decide requests.
  • submitterId and deciderId declared as userId(), and names stored as snapshots.

Workspace members as approvers

async function approvers(ctx: TeamContext): Promise<Member[]> {
  const members = await ctx.workspace.members();
  const allowed = parseEmailList(ctx.env.APPROVER_EMAILS);
  if (allowed.length === 0) return members;
  return members.filter((m) => allowed.includes(m.email.toLowerCase()));
}

The same helper runs in queries (to show the queue), mutations (to authorize decisions), and the schedule (to address the digest). Schedules may call ctx.workspace.members() even though they have no caller.

The digest

schedules: {
  digest: schedule("0 9 * * 1-5", async (ctx) => {
    await sendDigest(ctx);
  })
}

sendDigest reads up to 100 pending requests through the by_status index and emails each approver once. It never calls ctx.auth guards, because schedules run as the system. See schedules.

Configure

# .env.fuast.server
APPROVER_EMAILS=finance@acme.com,cto@acme.com

Without it, every workspace member can decide anyone else's request. For the hosted app, set it in the dashboard Env tab or deploy the file.

Try it

bunx @revnu/fuast dev

Submit as dev, approve as ?fuast_user=alice, and read the emails with bunx @revnu/fuast logs --port 3000. Click "Email digest now" to test the digest. Then:

bunx @revnu/fuast deploy
bunx @revnu/fuast share --visibility workspace