Skip to main content

Introduction

The Context object (ctx) provides a powerful API for programmatically interacting with your spreadsheet. It allows you to read and write cell values, manipulate rows, navigate between sheets, and orchestrate complex workflows—all from within your column formulas.

Available APIs

Getting Started

The ctx object is automatically available in all column formulas:
// Get a value from the current row
const email = ctx.thisRow.get("email");

// Set multiple values on the current row
ctx.thisRow.set({
   status: "processed",
   lastUpdated: new Date().toISOString(),
});

// Add a row to another sheet
const newRow = await ctx.sheet("Contacts").addRow({
   name: ctx.thisRow.get("name"),
   email: email,
   source: "Import",
});

// Query data by value
const company = await ctx.getRowByValue("Companies", "Acme Corp");

Key Features

  • Row Operations: Read and write data on the current row or any row in the sheet
  • Cross-Sheet Access: Push data between sheets and query related records
  • Foreign Key Navigation: Use dot notation to traverse relationships (e.g., Companies.Owner.name)
  • Workflow Control: Optimize execution with halt() for early termination signals
  • Type Safety: Column names and sheet names are validated at runtime
  • Metadata Management: Store custom metadata on cells for advanced use cases

Context Properties

The ctx object includes several key properties:
  • ctx.rowId - UUID of the current row
  • ctx.colId - UUID of the current column
  • ctx.thisRow - Helpers for the current row
  • ctx.thisCell - Helpers for the current cell (unavailable in webhooks)
  • ctx.thisColumn - Helpers for the current column
  • ctx.thisSheet - Helpers scoped to the current sheet
  • ctx.utils - Utility functions for data transformation

Common Patterns

// Get contact info from current row
const name = ctx.thisRow.get("Name");
const companyName = ctx.thisRow.get("Company");

// Find or create company account in Accounts sheet
const account = await ctx.sheet("Accounts").addRow({
   "Company Name": companyName,
   Status: "New",
});

// Enrich the contact's LinkedIn profile
const linkedinUrl = await services.person.linkedin.findUrl({
   name,
   company: companyName,
});

if (linkedinUrl) {
   const profile = await services.person.linkedin.enrich({ url: linkedinUrl });

   ctx.thisRow.set({
      "LinkedIn URL": linkedinUrl,
      Title: profile.job_title,
      Location: profile.location_city,
      "Account ID": account.id,
      Status: "Enriched",
   });
}

Qualify Leads and Skip Non-ICP

// Quick qualification check before expensive enrichment
const companyName = ctx.thisRow.get("Company");
const website = ctx.thisRow.get("Website");

// Scrape and analyze company website
const scraped = await services.scrape.website({ url: website });

const analysis = await services.ai.generateObject({
   prompt: `Based on this website, is ${companyName} in our ICP? 
   Our ICP: B2B SaaS companies, 50-500 employees, selling to enterprises.
   Website: ${scraped.markdown}`,
   schema: z.object({
      isICP: z.boolean(),
      reason: z.string(),
   }),
});

// Signal early termination if not a good fit
if (!analysis.object.isICP) {
   ctx.thisRow.set({
      Status: "Disqualified",
      "Disqualification Reason": analysis.object.reason,
   });
   ctx.halt("Not in ICP");
   return;
}

// Continue with expensive enrichment for qualified leads
ctx.thisRow.set({ Status: "Qualified - Enriching" });

Calculate Account Health Score

// Get all contacts and activities related to this account
const contacts = ctx.thisRow.getRelatedRows("Contacts");
const activities = ctx.thisRow.getRelatedRows("Activities");

// Calculate account metrics
const decisionMakers = contacts.filter((c) => c.get("Title")?.match(/(CEO|CTO|CFO|VP|Director)/i)).length;

const recentActivities = activities.filter((a) => {
   const date = new Date(a.get("Date"));
   const daysAgo = (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24);
   return daysAgo <= 30;
}).length;

const emailsSent = activities.filter((a) => a.get("Type") === "Email").length;
const meetingsHeld = activities.filter((a) => a.get("Type") === "Meeting").length;

// Calculate health score
let healthScore = 0;
healthScore += Math.min(decisionMakers * 10, 30); // Up to 30 points for contacts
healthScore += Math.min(recentActivities * 5, 40); // Up to 40 points for activity
healthScore += Math.min(meetingsHeld * 10, 30); // Up to 30 points for meetings

ctx.thisRow.set({
   "Total Contacts": contacts.length,
   "Decision Makers": decisionMakers,
   "Recent Activities": recentActivities,
   "Account Health Score": healthScore,
   "Account Status": healthScore >= 70 ? "Hot" : healthScore >= 40 ? "Warm" : "Cold",
});