Files
cfw-autumn/AGENTS.md
2026-04-16 15:59:41 +00:00

12 KiB

Project Context System

Projects maintain state in .context/<project>/ folders across sessions. Tasks are optional parallel workstreams within a project.

Reading context (session start)

When the user mentions a project or task name and .context/<name>/ exists:

  1. Read project STATUS.md first (20-30 line "resume card")
  2. If the project has tasks/, list active tasks
  3. If the user mentions a specific task, read tasks/<task>/STATUS.md
  4. Read the most recent session summary if more detail is needed
  5. Do NOT read everything upfront. Use progressive disclosure.

Updating context (at breakpoints, NOT continuously)

Update at these moments ONLY:

  • Phase or milestone completed
  • Architectural decision made (append to DECISIONS.md)
  • User says they're done or switching tasks
  • Blocker discovered or resolved
  • Task created, completed, or handed off

Do NOT update context during normal coding work. Work first, compact at breakpoints.

Compaction quality

STATUS.md must be:

  • Correct (reflects actual current state, not stale)
  • Complete (no critical information missing)
  • Concise (project < 30 lines, task < 20 lines)

Always REWRITE STATUS.md completely rather than append.

File structure

.context/<project>/
  STATUS.md       -- project resume card (includes Active Tasks section)
  PLAN.md         -- phases and architecture
  DECISIONS.md    -- append-only decision log
  sessions/       -- dated session summaries
  tasks/          -- optional parallel workstreams
    <task>/
      STATUS.md   -- task resume card
      DECISIONS.md

Task handoff

When a task is being handed to another agent, ensure the task's STATUS.md is up to date -- it's the handoff artifact.

scripts-v2 Conventions

File naming

  • Always kebab-case: standardize-stripe-state.ts, not standardizeStripeState.ts
  • Scripts go in runs/{org-name}/: e.g. runs/mintlify/fix-prices.ts
  • Never use index.ts for business logic — name files after what the function does

Imports

  • script / step: from "../lib" or "../../lib" (relative to script)
  • Types/enums: from "@autumn/shared" (AppEnv, FullProduct, Customer, etc.)
  • Server services: from "@autumn/server/src/internal/..." (CusService, ProductService, etc.)
  • Lib utilities: from "@autumn-cloud/lib/..." (createScriptContext, search helpers)

Function signatures

  • Always use named object params: fn({ db, orgId }) not fn(db, orgId)
  • This applies to all functions, even single-argument ones

Three categories of functions

1. Steps (step())

Named phases of work. Declare their own id and typed input/output. Called via s.run().

import { step, type ScriptContext } from "../../../lib";

export const standardizeBasePrices = step({
  id: "standardize-base-prices",
  run: async ({ ctx, group, priceCache }: {
    ctx: ScriptContext;
    group: SubscriptionGroup;
    priceCache: StripePriceCache;
  }): Promise<BasePriceMismatch[]> => {
    // business logic — returns typed data
    return mismatches;
  },
});

2. Orchestrators (steps that compose other steps)

Their run function takes s in addition to ctx + payload. They call s.run() to compose sub-steps.

export const processCustomer = step({
  id: "process-customer",
  run: async ({ s, ctx, customerId, priceCache }: {
    s: ScriptUtils;
    ctx: ScriptContext;
    customerId: string;
    priceCache: StripePriceCache;
  }) => {
    const groups = await s.run(loadSubscriptionGroups, { customerId });
    const result = await s.run(standardizeBasePrices, { group, priceCache });
    return { customerId, status: "ok", ...result };
  },
});

3. Utilities (plain functions)

Helpers that aren't meaningful workflow phases. Regular async functions, no step(). Examples: formatSubscriptionUpdate, listStripeSubscriptions, buildCacheKey.

Params rules

  • dryRun lives on ctx.dryRun — never pass as a separate param
  • data is accessed via s.data in orchestrators only — never threaded to leaf steps
  • If a step needs more than ctx + 3 business params, it's doing too much

Result convention (Trigger.dev-style ok pattern)

  • Every function returns typed data — never mutates caller's objects
  • Fallible steps return a discriminated union: { ok: true; ... } | { ok: false; reason: string }
  • The caller checks result.ok — just like Trigger.dev's triggerAndWait() result
  • Infallible steps return their typed data directly (e.g. BasePriceMismatch[])

Script structure

  • dryRun: true by default. Flip to false only when ready to mutate
  • All tunables go in the params block — never use CLI args or scattered top-of-file constants
  • Use s.step() for inline phases, s.run(step, payload) for named step functions
  • Use s.batch() for iterating over items — return objects from fn to collect results
  • Batch fn receives { item, ctx, s } — use s.run() inside batch for sub-steps
  • Use output: "csv" on batch to auto-write results
  • Use checkpoint: true on batch for resume across restarts
  • Always use ctx.logger.info() — never console.log()

Large scripts — folder organization

For non-trivial work, create a folder under runs/{org-name}/ and split into three subfolders:

runs/mintlify/standardize-stripe/
  standardize-stripe-state.ts            # entrypoint (script)
  config.ts                              # constants
  orchestrators/                         # steps that compose other steps via s.run()
    process-customer.ts
    process-subscription-group.ts
  steps/                                 # leaf steps (pure business logic, no s)
    load-customer-ids.ts
    load-exclude-list.ts
    load-subscription-groups.ts
    guards.ts
    check-customer-flags.ts
    standardize-base-prices.ts
    standardize-usage-prices.ts
    update-stripe-subscription.ts
    cancel-monthly-companion.ts
  utils/                                 # plain functions (no step(), not logged)
    find-or-create-matching-stripe-price.ts
    format-subscription-update.ts
    format-customer-result.ts

Folder rules:

  • orchestrators/ — takes { s, ctx, ... }, calls s.run() to compose steps
  • steps/ — takes { ctx, ... }, returns typed data, no s
  • utils/ — plain functions, no step(), no logging
  • Root: only the entrypoint and config

Entrypoint pattern:

run: async ({ s, ctx }) => {
  const customerIds = await s.step({
    id: "load customers",
    fn: () => loadCustomerIds({ ctx }),
  });

  await s.batch({
    id: "standardize",
    items: customerIds,
    output: "csv",
    fn: async ({ item: customerId, s: batchS }) => {
      return batchS.run(processCustomer, { customerId, priceCache });
    },
  });
},

Export pattern (for Trigger.dev compatibility)

const myScript = await script({ ... });
export default myScript;
if (import.meta.main) await myScript.run();

scripts-v2 API

script()

Self-executing entry point. The file IS the execution — hit ctrl+enter to run via ./run.sh.

import { script } from "../lib";
import { AppEnv } from "@autumn/shared";

const myScript = await script({
  id: "org-name/script-name",
  org: "autumn_org_id",
  env: AppEnv.Sandbox,
  dryRun: true,
  loadProducts: true,
  description: "What this does",

  params: {
    concurrency: 5,
    limit: 1 as number | null,
    only: null as string[] | null,
  },

  run: async ({ s, ctx }) => {
    // s = script utilities (step, batch, run, log, data)
    // ctx = data and services (extends AutumnContext)
  },
});

export default myScript;
if (import.meta.main) await myScript.run();

step()

Define a named step with typed input/output. Steps declare their own id — the caller just provides the business payload.

import { step, type ScriptContext } from "../lib";

export const loadCustomerGroups = step({
  id: "load-customer-groups",
  run: async ({ ctx, customerId }: {
    ctx: ScriptContext;
    customerId: string;
  }) => {
    // business logic
    return { fullCustomer, groups };
  },
});

Orchestrator steps (compose sub-steps) take s in addition to ctx:

export const processCustomer = step({
  id: "process-customer",
  run: async ({ s, ctx, customerId }: {
    s: ScriptUtils;
    ctx: ScriptContext;
    customerId: string;
  }) => {
    const { groups } = await s.run(loadCustomerGroups, { customerId });
    // ... compose more steps
  },
});

s.run()

Execute a step, auto-injecting ctx and s. The caller only passes business-specific payload.

// step defined elsewhere with id and typed run function
const result = await s.run(standardizeBasePrices, { group, priceCache });
//                          ^^ knows its own id        ^^ only business payload

TypeScript strips ctx and s from the call-site — you get autocomplete on just the business params and a typed return value.

ctx (ScriptContext)

Extends AutumnContext — pass directly to any server function.

Field Type Description
ctx.db DrizzleCli Postgres database client
ctx.stripe Stripe Stripe client (env/Connect-aware)
ctx.org Organization Loaded org object
ctx.env AppEnv Live or Sandbox
ctx.features Feature[] All features for this org
ctx.products FullProduct[] All products (empty if loadProducts: false)
ctx.dryRun boolean Whether this is a dry run
ctx.params P Typed params from your params block
ctx.logger Logger Pino-based logger (info/warn/error/debug/child)

s (ScriptUtils)

Field Type Description
s.step function Inline named section with auto-logging
s.batch function Batch processor with output, checkpoint, buffered logging
s.run function Execute a step() with auto-injected ctx/s
s.log ScriptLog Logger with table/json/section formatting
s.data ScriptData Data layer: store, list, write, read

s.step()

For inline phases (not defined as separate step() functions):

const data = await s.step({
  id: "load customers",
  fn: async () => CusService.getByOrg({ db: ctx.db, orgId: ctx.org.id, env: ctx.env }),
});

s.batch()

Batch fn receives { item, ctx, s } — use s.run() inside for sub-steps:

await s.batch({
  id: "process-customers",
  items: customerIds,
  output: "csv",
  checkpoint: true,

  fn: async ({ item: customerId, ctx, s }) => {
    return s.run(processCustomer, { customerId, priceCache });
  },

  concurrency: 5,
  limit: 10,
  onError: "continue",
});

BatchResult

const result = await s.batch({ ... });
result.processed   // number of items successfully processed
result.skipped     // number skipped (checkpoint or skipIf)
result.errors      // number of errors (when onError: "continue")
result.rows        // collected return values from fn
result.duration    // total ms

s.data (ScriptData)

Data dir lives at runs/<script-id>/data/ with four subdirs:

  • inputs/ — curated inputs (git-committed)
  • outputs/ — script results (git-committed)
  • state/ — checkpoints, resume tracking (gitignored)
  • logs/ — append-only audit trail (gitignored)
const finished = s.data.store<boolean>("state/finished");
finished.has("cus_123");
finished.set("cus_123", true);
finished.flush();

const results = s.data.list<AuditRow>("outputs/audit-results");
results.push({ customerId: "cus_123", status: "ok" });

s.data.write("outputs/report.csv", csvContent);
const config = s.data.read("inputs/exclude-orgs.csv");

params pattern

All tunables in one typed block. Edit and re-run — no CLI args.

params: {
  concurrency: 5,
  limit: 1 as number | null,
  only: null as string[] | null,
},

Batch automatically inherits concurrency, limit, only from params.

Running

./run.sh runs/org-name/my-script.ts
LOCAL_OVERRIDE=true ./run.sh runs/org-name/my-script.ts