TxScript for Tasks

Learn how to use TxScript to build scheduled automation tasks

Overview

TaskTx is the server-side SDK for building Scheduled Tasks - automated workflows that run on predefined schedules. Tasks are perfect for:

  • Automated reports and summaries
  • Scheduled reconciliation workflows
  • Data verification and validation
  • Email notifications and reminders
  • Automated data processing

Quick Start

Tasks run server-side on a schedule. Your task code receives a tx capability object:

// Example: Monthly spending report
const thisMonth = new Date();
const firstDay = new Date(thisMonth.getFullYear(), thisMonth.getMonth(), 1)
  .toISOString().split('T')[0];
const lastDay = new Date(thisMonth.getFullYear(), thisMonth.getMonth() + 1, 0)
  .toISOString().split('T')[0];

// Search invoices from this month
const invoices = await tx.searchEntities({
  query: "",
  filters: {
    docType: "INVOICE",
    date: { gte: firstDay, lte: lastDay }
  },
  limit: 1000
});

// Calculate total
let total = 0;
for (const invoice of invoices.results) {
  total += invoice.preview.amount || 0;
}

// Send email report
await tx.sendEmail({
  title: `Monthly Spending Report - ${thisMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}`,
  message: `Total spending this month: $${total.toLocaleString(undefined, { minimumFractionDigits: 2 })}`
});

// Create in-app notification
tx.addNotification({
  level: "info",
  message: `Monthly report sent. Total: $${total.toLocaleString()}`
});

Core API

Search Operations

// Search entities (same as uiTx)
const results = await tx.searchEntities({
  query: "vendor name",
  filters: {
    docType: "INVOICE",
    date: { gte: "2025-01-01", lte: "2025-12-31" },
    amount: { gte: 100 },
    currency: "USD"
  },
  limit: 100
});

Entity Operations

// Get full entity data
const entity = await tx.getEntity({ entityId: "doc123_items_0" });

// List documents
const documents = await tx.listDocuments({
  docType: "INVOICE",
  folderId: "folder123",
  date: { gte: "2025-01-01" }
});

Aggregation

// Aggregate data
const aggregated = await tx.aggregate({
  from: "entities",
  metrics: [
    { op: "sum", field: "amount", as: "total" },
    { op: "count", as: "count" }
  ],
  groupBy: ["month", "docType"],
  where: [
    { field: "docType", op: "=", value: "INVOICE" }
  ]
});

Reconciliation

// Create reconciliation link
const result = await tx.createReconciliationLink({
  sourceRef: { entityId: "invoice123", ... },
  targetRef: { entityId: "payment456", ... },
  amount: 1000.00,
  currency: "USD",
  edgeType: "paid_by"
});

// List reconciliation links
const links = await tx.listReconciliationLinks({
  entityId: "invoice123"
});

// Trace money flow
const trace = await tx.traceReconciliation({
  startEntityId: "invoice123",
  direction: "both",
  maxDepth: 10
});

Task-Specific Capabilities

Email Sending

// Send email to task's registered notification emails
await tx.sendEmail({
  title: "Report Title",
  message: "Report content here"
});

// Note: 'to' parameter is NOT required - uses task.notificationEmails automatically

App Notifications

// Create in-app notification (org-scoped)
tx.addNotification({
  level: "info", // "info" | "warning" | "error"
  message: "Task completed successfully"
});

Logging

// Log messages for debugging
tx.log("info", "Processing started");
tx.log("warn", "Rate limit approaching");
tx.log("error", "API request failed", { status: 500 });

Utilities

// Check if value is a non-empty string
if (tx.utils.isNonEmptyString(value)) {
  // value is guaranteed to be a string
}

// Parse number from string or number
const num = tx.utils.parseNumber("123.45");

// Check if value is nullish
if (tx.utils.isNullish(value)) {
  // value is null or undefined
}

// Format currency
const formatted = tx.utils.formatCurrency(1234.56, "USD");

Complete Examples

Example 1: Monthly Spending Report

// Calculate monthly spending by vendor
const thisMonth = new Date();
const firstDay = new Date(thisMonth.getFullYear(), thisMonth.getMonth(), 1)
  .toISOString().split('T')[0];
const lastDay = new Date(thisMonth.getFullYear(), thisMonth.getMonth() + 1, 0)
  .toISOString().split('T')[0];

// Aggregate by vendor
const data = await tx.aggregate({
  from: "entities",
  metrics: [{ op: "sum", field: "amount", as: "total" }],
  groupBy: ["vendorName"],
  where: [
    { field: "docType", op: "=", value: "INVOICE" },
    { field: "date", op: ">=", value: firstDay },
    { field: "date", op: "<=", value: lastDay }
  ]
});

// Build report
let report = `Monthly Spending Report - ${thisMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}\n\n`;
let grandTotal = 0;

for (const row of data.rows) {
  report += `${row.vendorName}: $${row.total.toLocaleString(undefined, { minimumFractionDigits: 2 })}\n`;
  grandTotal += row.total || 0;
}

report += `\nGrand Total: $${grandTotal.toLocaleString(undefined, { minimumFractionDigits: 2 })}`;

// Send email
await tx.sendEmail({
  title: `Monthly Spending Report - ${thisMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}`,
  message: report
});

tx.addNotification({
  level: "info",
  message: `Monthly report sent. Total: $${grandTotal.toLocaleString()}`
});

Example 2: Verify Invoice Received

// Check if invoice from VendorX was received this month
const thisMonth = new Date();
const firstDay = new Date(thisMonth.getFullYear(), thisMonth.getMonth(), 1)
  .toISOString().split('T')[0];
const lastDay = new Date(thisMonth.getFullYear(), thisMonth.getMonth() + 1, 0)
  .toISOString().split('T')[0];

const invoices = await tx.searchEntities({
  query: "VendorX",
  filters: {
    docType: "INVOICE",
    date: { gte: firstDay, lte: lastDay }
  },
  limit: 10
});

if (invoices.total === 0) {
  // No invoice found - send alert
  await tx.sendEmail({
    title: "⚠️ Missing Invoice Alert",
    message: `No invoice from VendorX found for ${thisMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}.`
  });
  
  tx.addNotification({
    level: "warning",
    message: "Missing invoice from VendorX this month"
  });
} else {
  tx.log("info", `Found ${invoices.total} invoice(s) from VendorX`);
}

Example 3: Automated Reconciliation

// Match invoices with payments from last 30 days
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const dateFrom = thirtyDaysAgo.toISOString().split('T')[0];
const dateTo = new Date().toISOString().split('T')[0];

// Search invoices
const invoices = await tx.searchEntities({
  query: "vendor X",
  filters: {
    docType: "INVOICE",
    date: { gte: dateFrom, lte: dateTo }
  },
  limit: 100
});

// Search payments
const payments = await tx.searchEntities({
  query: "vendor Y",
  filters: {
    docType: "PAYMENT",
    date: { gte: dateFrom, lte: dateTo }
  },
  limit: 100
});

// Match by amount and currency
let matchesCreated = 0;
const matchDetails = [];

for (const invoice of invoices.results) {
  const invoiceEntity = await tx.getEntity({ entityId: invoice.ref.entityId });
  const invoiceAmount = invoiceEntity.data.amount;
  const invoiceCurrency = invoiceEntity.data.currency;
  
  for (const payment of payments.results) {
    const paymentEntity = await tx.getEntity({ entityId: payment.ref.entityId });
    const paymentAmount = paymentEntity.data.amount;
    const paymentCurrency = paymentEntity.data.currency;
    
    // Match if amounts are equal (within 0.01 tolerance) and currencies match
    if (paymentCurrency === invoiceCurrency && 
        Math.abs(paymentAmount - invoiceAmount) < 0.01) {
      
      const result = await tx.createReconciliationLink({
        sourceRef: invoice.ref,
        targetRef: payment.ref,
        amount: invoiceAmount,
        currency: invoiceCurrency,
        edgeType: "paid_by"
      });
      
      if (result.ok) {
        matchesCreated++;
        matchDetails.push({
          invoice: invoice.preview.description,
          payment: payment.preview.description,
          amount: invoiceAmount,
          currency: invoiceCurrency
        });
      }
    }
  }
}

// Send report
const report = `Reconciliation Report (Last 30 Days)\n\n` +
  `Matches Created: ${matchesCreated}\n\n` +
  matchDetails.map(m => 
    `- ${m.invoice} ↔ ${m.payment} (${m.currency} ${m.amount})`
  ).join("\n");

await tx.sendEmail({
  title: "Reconciliation Report",
  message: report
});

tx.addNotification({
  level: "info",
  message: `Created ${matchesCreated} reconciliation link(s)`
});

Security & Limits

  • Email limits: Maximum 10 emails per task execution
  • Notification limits: Maximum 50 notifications per task execution
  • HTTP calls: Maximum 10 calls per execution
  • Execution timeout: 30 seconds total execution time
  • Rate limits: Email rate limit of 1 email per second per task

Task Context

Tasks run with the following context:

  • orgId: Organization ID (automatically set)
  • taskId: Task ID (automatically set)
  • userId: User who created the task
  • notificationEmails: Emails registered for the task (used by sendEmail)

Tasks are org-wide - they don't have document context like formula fields do.

Best Practices

  1. Handle errors gracefully: Wrap operations in try-catch blocks
  2. Use aggregation: For large datasets, use aggregate() instead of iterating
  3. Log important events: Use tx.log() for debugging
  4. Send meaningful reports: Include context and actionable information in emails
  5. Create notifications: Use addNotification() for in-app visibility