Skip to main content

Documentation Index

Fetch the complete documentation index at: https://glide-9da73dea.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

This recipe builds spend-export from scratch — a skill that lets an agent pull a structured CSV of spend data for a given account and date range. The skill follows the same scaffold used by every package in the OSS Cathedral. Audience: contributors who want to author and publish a new @glideco/* skill.

Prerequisites

Steps

1. Scaffold the skill package

pnpm glide skill:new --slug spend-export
This creates packages/skills/spend-export/ with:
packages/skills/spend-export/
  src/
    index.ts          # skill entry point
    manifest.ts       # SkillManifest definition
    policy.ts         # default policy template
    consent.ts        # consent flow descriptor
    schema.ts         # input/output zod schemas
  tests/
    smoke.test.ts
  package.json
  tsconfig.json

2. Write the SkillManifest

// src/manifest.ts
import type { SkillManifest } from '@glideco/schemas';

export const manifest: SkillManifest = {
  schemaVersion: '1.0.0',
  skillId: 'spend-export',
  displayName: 'Spend Export',
  description: 'Exports a structured CSV of account spend for a given date range.',
  version: '0.1.0',
  author: 'Glide Contributors',
  license: 'MIT',
  requiredScopes: ['transactions.list', 'accounts.balance'],
  outputFormats: ['csv', 'json'],
  consentRequired: true,
  sandboxSupported: true,
  docsUrl: 'https://github.com/darshanbathija/axtior-neobank/tree/main/examples/build-a-skill-spend-export',
};
Validate:
pnpm glide skill:validate packages/skills/spend-export

3. Define input/output schemas

// src/schema.ts
import { z } from 'zod';

export const SpendExportInput = z.object({
  accountId: z.string(),
  from: z.string().datetime({ offset: true }),
  to: z.string().datetime({ offset: true }),
  format: z.enum(['csv', 'json']).default('csv'),
});

export const SpendExportOutput = z.object({
  rowCount: z.number(),
  totalUsdc: z.string(),
  exportUrl: z.string().url(),
  expiresAt: z.string().datetime({ offset: true }),
});

export type SpendExportInput = z.infer<typeof SpendExportInput>;
export type SpendExportOutput = z.infer<typeof SpendExportOutput>;

4. Write the policy template

The policy template is the minimum AgentPolicyEnvelope a principal must grant before this skill can execute. It is advisory — the gateway always checks the actual live envelope at call time.
// src/policy.ts
import type { AgentPolicyEnvelope } from '@glideco/schemas';

export function policyTemplate(accountId: string): Partial<AgentPolicyEnvelope> {
  return {
    grantedSkills: ['spend-export'],
    dataScopes: ['transactions.list', 'accounts.balance'],
    spendControls: {
      perTransactionCapUsdc: '0.00', // read-only skill — no spend
    },
    accountIds: [accountId],
  };
}
// src/consent.ts
import type { ConsentFlow } from '@glideco/schemas';

export const consentFlow: ConsentFlow = {
  version: '1.0',
  prompts: [
    {
      id: 'scope-acknowledge',
      type: 'checkbox',
      label: 'Allow this skill to read your transaction history for the selected date range.',
      required: true,
    },
    {
      id: 'data-retention',
      type: 'select',
      label: 'How long should the export URL stay valid?',
      options: [
        { value: '1h', label: '1 hour' },
        { value: '24h', label: '24 hours' },
        { value: '7d', label: '7 days' },
      ],
      default: '24h',
    },
  ],
};

6. Write the skill entry point

// src/index.ts
import { SpendExportInput, SpendExportOutput } from './schema';
import type { SkillContext } from '@glideco/schemas';

export { manifest } from './manifest';
export { consentFlow } from './consent';
export { policyTemplate } from './policy';

export async function run(
  input: SpendExportInput,
  ctx: SkillContext,
): Promise<SpendExportOutput> {
  const validated = SpendExportInput.parse(input);

  // Use ctx.rpc to call the MCP read endpoint — ctx handles auth + token refresh.
  const txList = await ctx.rpc<{ transactions: Transaction[] }>('read', 'transactions.list', {
    accountId: validated.accountId,
    from: validated.from,
    to: validated.to,
    limit: 10_000,
  });

  const totalUsdc = txList.transactions
    .reduce((sum, tx) => sum + parseFloat(tx.amountUsdc), 0)
    .toFixed(2);

  const exportUrl = await ctx.storage.uploadCsv(
    `spend-export-${validated.accountId}-${Date.now()}.csv`,
    txList.transactions,
    { ttl: ctx.consentAnswers['data-retention'] ?? '24h' },
  );

  return SpendExportOutput.parse({
    rowCount: txList.transactions.length,
    totalUsdc,
    exportUrl,
    expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
  });
}

7. Write the smoke test

// tests/smoke.test.ts
import { describe, it, expect, vi } from 'vitest';
import { run } from '../src/index';
import type { SkillContext } from '@glideco/schemas';

const mockCtx: SkillContext = {
  rpc: vi.fn().mockResolvedValue({
    transactions: [
      { txId: 'tx_001', amountUsdc: '50.00', timestamp: '2026-05-01T10:00:00Z' },
      { txId: 'tx_002', amountUsdc: '120.00', timestamp: '2026-05-02T14:30:00Z' },
    ],
  }),
  storage: {
    uploadCsv: vi.fn().mockResolvedValue('https://storage.glide.co/exports/demo.csv'),
  },
  consentAnswers: { 'data-retention': '24h' },
};

describe('spend-export smoke', () => {
  it('returns correct row count and total', async () => {
    const result = await run(
      {
        accountId: 'acc_demo_01',
        from: '2026-05-01T00:00:00Z',
        to: '2026-05-03T00:00:00Z',
        format: 'csv',
      },
      mockCtx,
    );
    expect(result.rowCount).toBe(2);
    expect(result.totalUsdc).toBe('170.00');
    expect(result.exportUrl).toMatch(/^https:\/\//);
  });
});
Run:
pnpm --filter spend-export test

Run it

pnpm --filter spend-export test
Expected output:
✓ spend-export smoke > returns correct row count and total (11ms)

Test Files  1 passed
Tests       1 passed
Duration    312ms

Extend it

  • Add a json format branch to run() that uploads a JSONL file instead of CSV.
  • Add a maxRows cap and return a truncated: true flag in the output when hit.
  • Wire the skill into the MCP gateway by registering it in apps/mcp/src/skills/registry.ts.
  • Publish under @glideco/skill-spend-export using node scripts/publish-glide-skill.mjs spend-export.
  • Add an anomaly heuristic that fires when the exported row count exceeds 2× the account’s 30-day average.

Source

github.com/darshanbathija/axtior-neobank/tree/main/examples/build-a-skill-spend-export

Reading list