Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.goldsky.com/llms.txt

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

Manage persistent state across tasks and task runs using MongoDB-like collection operations. Most applications have some notion of stored state — collections are where that state lives, so any task can read, write, search, and filter it at any time. When running locally, collections use SQLite (stored in .compose/stage.db). When deployed, each Compose app gets its own fully isolated Postgres database. The API is identical in both environments. The hosted Postgres is yours to use for application state via collections, but Compose also uses the same database internally to power durable execution (clean resumes after crashes and rolling deploys), transaction reorg monitoring, and wallet bookkeeping. Internal state lives in reserved namespaces — see Reserved collection names below.

Examples

import { TaskContext } from "compose";

type Dog = {
  breed: string;
  color: string;
};

export async function main({ collection, fetch }: TaskContext) {
  const dogsCollection = await collection<Dog>("dogs", [
    { path: "color", type: "text" },
  ]);

  // Get existing state
  const brownDogs = await dogsCollection.findMany({ color: "brown" });

  // Get some data
  const newData = await fetch<Dog>("https://api.dogs.com/v1/dogs/labrador");

  // Update state
  await dogsCollection.insertOne(newData);

  return {
    success: true,
  };
}

Full Interface

export type ScalarIndexType = "text" | "numeric" | "boolean" | "timestamptz";

export interface CollectionIndexSpec {
  path: string;
  type: ScalarIndexType;
  unique?: boolean;
}

export interface FindOptions {
  limit?: number;
  offset?: number;
}

// Filter helpers for comparison operators
export type FilterHelper =
  | "$gt"
  | "$gte"
  | "$lt"
  | "$lte"
  | "$in"
  | "$ne"
  | "$nin"
  | "$exists";
export type HelperValue = Partial<
  Record<FilterHelper, string | number | boolean | string[] | number[]>
>;
export type FilterValue = string | number | boolean | HelperValue;
export type Filter = Record<string, FilterValue>;

export type WithId<T> = T & { id: string };

export interface Collection<TDoc = unknown> {
  readonly name: string;
  insertOne(doc: TDoc, opts?: { id?: string }): Promise<{ id: string }>;
  findOne(filter: Filter): Promise<WithId<TDoc> | null>;
  findMany(filter: Filter, options?: FindOptions): Promise<Array<WithId<TDoc>>>;
  getById(id: string): Promise<WithId<TDoc> | null>;
  /**
   * @param opts.upsert - Defaults to true. Set to false to throw if document doesn't exist.
   */
  setById(
    id: string,
    doc: TDoc,
    opts?: { upsert?: boolean },
  ): Promise<{ id: string; upserted?: boolean; matched?: number }>;
  deleteById(id: string): Promise<{ deletedCount: number }>;
  drop(): Promise<void>;
}
The collection function is available on TaskContext:
collection: <T>(
  name: string,
  indexes?: CollectionIndexSpec[],
) => Promise<Collection<T>>;

Filter operators

The Filter type supports comparison operators beyond simple equality:
// Equality (default)
await dogs.findMany({ color: "brown" });

// Comparison operators
await dogs.findMany({ age: { $gt: 3 } });        // greater than
await dogs.findMany({ age: { $gte: 3 } });       // greater than or equal
await dogs.findMany({ age: { $lt: 10 } });       // less than
await dogs.findMany({ age: { $lte: 10 } });      // less than or equal
await dogs.findMany({ color: { $ne: "brown" } }); // not equal

// Set membership
await dogs.findMany({ breed: { $in: ["labrador", "golden"] } });  // in set
await dogs.findMany({ breed: { $nin: ["chihuahua"] } });          // not in set

// Field existence
await dogs.findMany({ nickname: { $exists: true } });

setById return value

setById defaults to upsert behavior. The return value tells you whether the write created a new row or updated an existing one:
  • upserted: false, matched: 0 — the document did not exist, so a new row was inserted.
  • upserted: true, matched: 1 — an existing document with that id was updated.
Pass { upsert: false } to disable insert-on-missing; setById will then throw if no document exists with that id.

drop() vs deleteById()

collection.drop() runs DROP TABLE IF EXISTS against the backing database — it removes the collection’s table entirely, not just its rows. Subsequent calls like insertOne do not automatically recreate the table; you must call ctx.collection(name, indexes) again to recreate it (along with its indexes). For routine cleanup that preserves the table and its indexes, prefer deleteById (or iterate with findMany + deleteById). Reach for drop() only when you truly want to discard the collection.

Reserved collection names

The following names are reserved for internal use and cannot be used as collection names: wallets, stage, monitored_transactions, runs, context_functions. Attempting to create or drop a collection with a reserved name throws an error.

Next Steps

EVM

Interact with EVM blockchains and smart contracts.

env

Set and access environment variables.