Back to catalog

smartsuite-patterns

SmartSuite API patterns and field format validation. Covers choices vs options distinction (UUID corruption prevention), dry_run safety patterns, field format validation, and MCP integration best practices. Critical for preventing UUID corruption bugs.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: smartsuite-patterns
description: SmartSuite API patterns and field format validation. Covers choices vs options distinction (UUID corruption prevention), dry_run safety patterns, field format validation, and MCP integration best practices. Critical for preventing UUID corruption bugs.
allowed-tools: Read
---

# SmartSuite Patterns Skill

## Purpose

Provides SmartSuite API patterns, field format requirements, and UUID corruption prevention strategies. Critical for correct SmartSuite integration and data integrity.

## When to Use This Skill

Auto-activates when:
- Working with SmartSuite API
- Creating or updating SmartSuite fields
- "choices vs options" queries
- UUID corruption prevention
- SmartSuite field format validation
- SmartSuite MCP integration

---

## ⚠️ CRITICAL: UUID Corruption Prevention

### THE GOLDEN RULE: Use `choices`, NEVER `options`

**WRONG** (destroys all data):
```json
{
  "params": {
    "options": [  // ❌ WRONG - creates new UUIDs
      {"value": "draft"},
      {"value": "active"}
    ]
  }
}
```

**CORRECT** (preserves UUIDs):
```json
{
  "params": {
    "choices": [  // ✅ CORRECT - preserves UUIDs
      {"value": "draft", "id": "existing-uuid-1"},
      {"value": "active", "id": "existing-uuid-2"}
    ]
  }
}
```

**Why This Matters**: Using `options` instead of `choices` **permanently destroys all data** in that field across all records. This is IRREVERSIBLE.

---

## Authentication Pattern

```json
{
  "Authorization": "Token ${SMARTSUITE_API_TOKEN}",
  "ACCOUNT-ID": "s3qnmox1",
  "Content-Type": "application/json"
}
```

**CRITICAL**: Use "Token" not "Bearer" for Authorization header

---

## Rate Limiting

- **Max**: 4-5 requests/second
- **Recommended delay**: 250ms between requests
- **429 backoff**: 30 seconds with exponential backoff
- **Retry strategy**: Max 3 retries for 5xx errors

---

## Performance Benchmarks (Production Tested)

- **Query Operations**: 400-600ms consistently
- **CRUD Operations**: 400-700ms including validation
- **Bulk Operations**: 600-850ms for multiple records
- **Schema Discovery**: 300-500ms (standard), 450-850ms (intelligent API)

---

## Core CRUD Operations

### List Records (POST, not GET)
```json
POST /api/v1/applications/{appId}/records/list/
{
  "limit": 2,  // MCP safe limit
  "offset": 0,
  "filter": {
    "operator": "and",
    "fields": [
      {"field": "status", "comparison": "is", "value": "active"}
    ]
  }
}
```

### Get Single Record
```
GET /api/v1/applications/{appId}/records/{recordId}/
```
**CRITICAL**: Trailing slash required

### Create Record
```json
POST /api/v1/applications/{appId}/records/
{
  "title": "Record Title",
  "projects_link": ["68a8ff5237fde0bf797c05b3"],  // Array for linked records
  "status": "draft"
}
```

### Update Record
```json
PATCH /api/v1/applications/{appId}/records/{recordId}/
{
  "status": "active",
  "assigned_to": ["user_id_1", "user_id_2"]  // Array for linked records
}
```

### Delete Record
```
DELETE /api/v1/applications/{appId}/records/{recordId}/
```
**CRITICAL**: Trailing slash required or silent failure

---

## Bulk Operations (Max 25 records)

### Bulk Create
```json
POST /api/v1/applications/{appId}/records/bulk/
{
  "items": [
    {"title": "Record 1", "status": "draft"},
    {"title": "Record 2", "status": "active"}
  ]
}
```
**Performance**: 600-850ms for multiple records

### Bulk Update
```json
PATCH /api/v1/applications/{appId}/records/bulk/
{
  "items": [
    {"id": "record_id_1", "status": "completed"},
    {"id": "record_id_2", "priority": "high"}
  ]
}
```

---

## Critical Field Format Rules

### 1. Linked Record Fields
- **MUST** be arrays, even for single values
- **Wrong**: `"project_id": "68a8ff5237fde0bf797c05b3"`
- **Correct**: `"project_id": ["68a8ff5237fde0bf797c05b3"]`
- **Filter operators**: Use `has_any_of`, `has_all_of`, `has_none_of` - NEVER `is` or `equals`

### 2. Rich Text Fields (SmartDoc Structure)

**Populated Rich Text Field**:
```json
{
  "projectBrief": {
    "data": {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "attrs": {"textAlign": "left", "size": "medium"},
          "content": [
            { "type": "text", "text": "Your content here..." }
          ]
        }
      ]
    },
    "html": "<div class=\"rendered\"><p>Your content here...</p></div>",
    "preview": "Your content here..."
  }
}
```

**Empty Rich Text Field**:
```json
{
  "description": {
    "data": {"type": "doc", "content": []},
    "html": "",
    "preview": ""
  }
}
```

### 3. Select/Status Fields
- Use option **codes** not display labels
- **Wrong**: `"status": "In Progress"`
- **Correct**: `"status": "in_progress"`

### 4. Checklist Fields
Must use full SmartDoc structure with items array:
```json
{
  "checklist99": {
    "items": [
      {
        "id": "item-id",
        "content": { /* SmartDoc structure */ },
        "completed": false,
        "assignee": null,
        "due_date": null
      }
    ]
  }
}
```

### 5. Date Range Fields
```json
{
  "date_range": {
    "from_date": {"date": "2025-01-15T00:00:00Z", "include_time": false},
    "to_date": {"date": "2025-01-20T00:00:00Z", "include_time": false}
  }
}
```

### 6. Formula/Rollup Fields
- **READ ONLY** - cannot be set via API
- Values are calculated by SmartSuite
- Attempting to set will cause validation errors

---

## Field Discovery Process

### Step 1: Use Schema Discovery
```typescript
// Always start with schema discovery
const schema = await smartsuiteSchema(tableId, 'summary');
// Returns: table info, field list with types
```

### Step 2: Get Field Details
```typescript
// For detailed field information including choices
const fieldDetail = await smartsuiteSchema(tableId, 'field_detail', fieldSlug);
// Returns: Full field configuration including UUIDs
```

### Step 3: Validate Before Write
```typescript
// ALWAYS validate field format before writing
// - Linked records: Check for arrays
// - Select fields: Check for codes not labels
// - Required fields: Ensure present
```

---

## DRY-RUN Safety Pattern

**ALWAYS** default to `dry_run: true` for mutations:

```typescript
// Field creation with dry-run
smartsuiteFieldCreate(tableId, fieldConfig, dryRun: true)

// Field update with dry-run
smartsuiteFieldUpdate(tableId, fieldId, updates, dryRun: true)

// Record mutation with dry-run
smartsuiteRecord('update', tableId, recordId, data, dryRun: true)
```

**Only set `dryRun: false` after verifying operation is correct**

---

## Common Patterns

### Pattern 1: Safe Field Update (Preserving UUIDs)
```typescript
// 1. Get existing field configuration
const fieldDetail = await smartsuiteSchema(tableId, 'field_detail', 'status');

// 2. Extract existing choices with UUIDs
const existingChoices = fieldDetail.params.choices;
// [
//   {"value": "draft", "id": "uuid-1"},
//   {"value": "active", "id": "uuid-2"}
// ]

// 3. Add new choice while preserving existing UUIDs
const updatedChoices = [
  ...existingChoices,  // Preserve existing with UUIDs
  {"value": "archived", "id": generateNewUUID()}  // Add new
];

// 4. Update field with DRY-RUN first
await smartsuiteFieldUpdate(
  tableId,
  fieldId,
  { params: { choices: updatedChoices } },  // ✅ choices, not options
  dryRun: true
);

// 5. Review output, then execute
await smartsuiteFieldUpdate(
  tableId,
  fieldId,
  { params: { choices: updatedChoices } },
  dryRun: false
);
```

### Pattern 2: Creating Linked Record
```typescript
// WRONG: String value
const wrong = {
  project_id: "68a8ff5237fde0bf797c05b3"  // ❌ Will fail
};

// CORRECT: Array of strings
const correct = {
  project_id: ["68a8ff5237fde0bf797c05b3"]  // ✅ Works
};

// Create record
await smartsuiteRecord('create', tableId, null, correct);
```

### Pattern 3: Filtering Linked Records
```typescript
// WRONG: Using 'is' operator
const wrongFilter = {
  field: "project_id",
  comparison: "is",  // ❌ Won't work for linked records
  value: "project-uuid"
};

// CORRECT: Using 'has_any_of' operator
const correctFilter = {
  field: "project_id",
  comparison: "has_any_of",  // ✅ Correct for linked records
  value: ["project-uuid"]  // Array format
};

// Query with filter
await smartsuiteQuery('search', tableId, {
  filter: {
    operator: "and",
    fields: [correctFilter]
  }
});
```

---

## Anti-Patterns to Avoid

❌ **Using `options` instead of `choices` for select field updates**
✅ Always use `choices` with preserved UUIDs

❌ **Single values for linked record fields**
✅ Always use arrays, even for single links

❌ **Display labels for select field values**
✅ Use option codes (e.g., "in_progress" not "In Progress")

❌ **Skipping dry-run for mutations**
✅ Always dry-run first, then execute

❌ **Using `is` operator for linked record filters**
✅ Use `has_any_of`, `has_all_of`, or `has_none_of`

❌ **Attempting to set formula/rollup field values**
✅ These are read-only, calculated by SmartSuite

❌ **Forgetting trailing slashes in DELETE/GET requests**
✅ Always include trailing slash or expect silent failures

---

## Key Takeaways

1. **UUID preservation**: Use `choices` not `options` - this is CRITICAL
2. **Linked records**: Always arrays, even for single values
3. **DRY-RUN first**: Default to dry_run=true for all mutations
4. **Field discovery**: Use schema tools before writing
5. **Select field codes**: Use codes not display labels
6. **Filter operators**: `has_any_of` for linked records, not `is`
7. **Trailing slashes**: Required for GET/DELETE or silent failure
8. **Formula fields**: Read-only, don't attempt to set
9. **Rate limiting**: 250ms delay between requests recommended
10. **SmartDoc structure**: Rich text requires full document structure