HotCRM Logo

Metadata Reference

Comprehensive metadata examples for @objectstack/spec — covering data, UI, automation, AI, and integration patterns

Metadata Reference

This guide provides representative examples of every major metadata type in @objectstack/spec. Use these patterns as a starting point when building HotCRM packages.

Data Metadata

Object Definition

Every business entity starts with an ObjectSchema. Fields use the Field factory for strict typing.

// packages/crm/src/account.object.ts
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Account = ObjectSchema.create({
  name: 'account',
  label: 'Account',
  pluralLabel: 'Accounts',
  icon: 'building',
  description: 'Companies and organizations',

  fields: {
    name: Field.text({ label: 'Account Name', required: true, unique: true, maxLength: 255, searchable: true }),
    type: Field.select({
      label: 'Account Type',
      options: [
        { label: 'Prospect', value: 'Prospect' },
        { label: 'Customer', value: 'Customer' },
        { label: 'Partner', value: 'Partner' }
      ],
      defaultValue: 'Prospect'
    }),
    annual_revenue: Field.currency({ label: 'Annual Revenue', precision: 2, min: 0 }),
    parent_account: Field.lookup({ label: 'Parent Account', reference_to: 'account', cascade_delete: false }),
    created_date: Field.datetime({ label: 'Created Date', readonly: true, defaultValue: 'NOW()' })
  },

  enable: { searchable: true, trackHistory: true, feeds: true, files: true, apiEnabled: true }
});

Field Types

All field types available through the Field factory:

fields: {
  // Text types
  text_field:      Field.text({ label: 'Text', maxLength: 255 }),
  textarea_field:  Field.textarea({ label: 'Textarea', maxLength: 5000 }),
  richtext_field:  Field.richtext({ label: 'Rich Text' }),

  // Numeric types
  number_field:    Field.number({ label: 'Number', precision: 2 }),
  currency_field:  Field.currency({ label: 'Currency', precision: 2 }),
  percent_field:   Field.percent({ label: 'Percent', precision: 1 }),

  // Date/time types
  date_field:      Field.date({ label: 'Date' }),
  datetime_field:  Field.datetime({ label: 'DateTime' }),

  // Selection types
  select_field:    Field.select({ label: 'Select', options: [{ label: 'A', value: 'a' }] }),
  multiselect_field: Field.multiselect({ label: 'Multi-Select', options: [{ label: 'T1', value: 't1' }] }),

  // Relationships
  lookup_field:    Field.lookup({ label: 'Lookup', reference_to: 'account' }),
  master_detail:   Field.masterDetail({ label: 'Master-Detail', reference_to: 'account', cascade_delete: true }),

  // Communication
  email_field:     Field.email({ label: 'Email' }),
  phone_field:     Field.phone({ label: 'Phone' }),
  url_field:       Field.url({ label: 'URL' }),

  // Auto-generated
  autonumber_field: Field.autonumber({ label: 'Auto Number', format: 'SHO-{YYYY}-{000000}' }),

  // Computed
  formula_field:   Field.formula({ label: 'Formula', formula: 'quantity * unit_price', returnType: 'currency' }),
  summary_field:   Field.summary({ label: 'Summary', summarizedObject: 'opportunity', summaryType: 'sum', field: 'amount' }),

  // Special
  boolean_field:   Field.boolean({ label: 'Boolean' }),
  geolocation_field: Field.geolocation({ label: 'Location' }),
  encrypted_field: Field.encrypted({ label: 'Encrypted Data' }),
  json_field:      Field.json({ label: 'JSON Data' })
}

Relationships

Relationships are expressed through lookup fields, master-detail, and rollup summaries.

// packages/crm/src/opportunity.object.ts
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Opportunity = ObjectSchema.create({
  name: 'opportunity',
  label: 'Opportunity',

  fields: {
    name: Field.text({ label: 'Opportunity Name', required: true }),

    // Master-detail — cascade deletes with parent
    account: Field.masterDetail({
      label: 'Account', reference_to: 'account', cascade_delete: true, required: true
    }),

    // Lookup — no cascade
    primary_contact: Field.lookup({
      label: 'Primary Contact', reference_to: 'contact', cascade_delete: false
    }),

    // Rollup summary — aggregate child records
    total_quote_amount: Field.summary({
      label: 'Total Quote Amount', summarizedObject: 'quote',
      summaryType: 'sum', field: 'total_amount', filter: [['status', '=', 'Approved']]
    }),

    // Formula — cross-object reference
    days_to_close: Field.formula({ label: 'Days to Close', formula: 'close_date - TODAY()', returnType: 'number' }),
    account_owner: Field.formula({ label: 'Account Owner', formula: 'account.owner', returnType: 'lookup', reference_to: 'user' })
  }
});

UI Metadata

Page Layout

Page layouts define record detail views with field sections, related lists, and custom components.

// packages/crm/src/account.page.ts
import { PageSchema } from '@objectstack/spec/ui';

export const AccountPage = PageSchema.create({
  name: 'account_detail',
  object: 'account',
  type: 'record',

  layout: {
    type: 'tabs',  // or 'accordion', 'wizard'
    sections: [
      {
        label: 'Account Information',
        columns: 2,
        fields: ['name', 'account_number', 'type', 'industry', 'phone', 'website', 'annual_revenue']
      },
      {
        label: 'Opportunities',
        type: 'related_list',
        object: 'opportunity',
        columns: ['name', 'stage', 'amount', 'close_date'],
        filters: [['stage', '!=', 'Closed Lost']],
        sort: [{ field: 'close_date', direction: 'asc' }],
        actions: ['new', 'edit', 'delete']
      },
      {
        label: 'AI Insights',
        type: 'component',
        component: 'AccountAIInsights',
        props: { showHealthScore: true, showChurnRisk: true }
      }
    ]
  },

  actions: [
    { name: 'edit', label: 'Edit', type: 'standard' },
    { name: 'ai_analyze', label: 'AI Analyze', type: 'custom', handler: 'analyzeWithAI', icon: 'sparkles' }
  ]
});

List Views

List views control column layout, filters, sorting, and inline editing for record tables.

// packages/crm/src/account.view.ts
import { ListView } from '@objectstack/spec/ui';

export const AccountListViews = {
  allAccounts: ListView.create({
    name: 'all_accounts',
    label: 'All Accounts',
    object: 'account',
    columns: [
      { field: 'name', width: 250, sortable: true, link: true },
      { field: 'type', width: 120, sortable: true },
      { field: 'annual_revenue', width: 150, sortable: true, align: 'right' },
      { field: 'owner', width: 150 },
      { field: 'created_date', width: 150, sortable: true, format: 'YYYY-MM-DD' }
    ],
    sort: [{ field: 'name', direction: 'asc' }],
    bulkActions: ['delete', 'update_owner', 'export'],
    inlineEdit: true,
    pagination: { pageSize: 25, options: [10, 25, 50, 100] }
  }),

  enterpriseAccounts: ListView.create({
    name: 'enterprise_accounts',
    label: 'Enterprise Accounts',
    object: 'account',
    filters: [
      { field: 'annual_revenue', operator: '>', value: 10000000 },
      { field: 'type', operator: '=', value: 'Customer' }
    ],
    columns: [
      { field: 'name', width: 250 },
      { field: 'annual_revenue', width: 150 },
      { field: 'industry', width: 150 }
    ],
    sort: [{ field: 'annual_revenue', direction: 'desc' }]
  })
};

Automation Metadata

Workflow Rules

Workflows trigger actions (field updates, emails, tasks, HTTP calls) on record events.

// packages/crm/src/lead_assignment.workflow.ts
import { WorkflowRule } from '@objectstack/spec/automation';

export const LeadAutoAssignment = WorkflowRule.create({
  name: 'lead_auto_assignment',
  label: 'Auto Assign Leads',
  object: 'lead',
  triggerType: 'onCreate',  // or 'onUpdate', 'onDelete'
  condition: 'status = "New" AND owner = NULL',

  actions: [
    { type: 'fieldUpdate', field: 'owner_id', formula: 'getNextAvailableRep(territory, industry)' },
    { type: 'emailAlert', template: 'new_lead_assigned', recipients: ['owner_id'] },
    { type: 'taskCreation', subject: 'Follow up: ${name}', assignee: '${owner_id}', dueDate: 'TODAY() + 1', priority: 'High' },
    { type: 'httpCall', method: 'POST', url: 'https://api.slack.com/webhooks/...',
      headers: { 'Content-Type': 'application/json' },
      body: { text: 'New lead assigned: ${name} to ${owner.name}' } }
  ],

  executionOrder: 1,
  active: true
});

Approval Process

Multi-step approval flows with conditional routing and per-step actions.

// packages/products/src/quote_approval.workflow.ts
import { ApprovalProcess } from '@objectstack/spec/automation';

export const QuoteDiscountApproval = ApprovalProcess.create({
  name: 'quote_discount_approval',
  object: 'quote',
  triggerType: 'onUpdate',
  condition: 'discount_percent > 10 AND status = "Draft"',

  initialSubmissionActions: [
    { type: 'fieldUpdate', field: 'approval_status', value: 'Pending' },
    { type: 'emailAlert', template: 'approval_request_submitted', recipients: ['${submitter}'] }
  ],

  steps: [
    {
      stepNumber: 1, name: 'sales_manager_approval',
      approverType: 'user', approver: '${owner.manager}',
      skipCondition: 'discount_percent <= 15',
      approvalActions: [{ type: 'fieldUpdate', field: 'approval_status', value: 'Manager Approved' }],
      rejectionActions: [
        { type: 'fieldUpdate', field: 'approval_status', value: 'Rejected' },
        { type: 'emailAlert', template: 'approval_rejected', recipients: ['${owner}'] }
      ]
    },
    {
      stepNumber: 2, name: 'sales_director_approval',
      approverType: 'role', approver: 'sales_director',
      skipCondition: 'discount_percent <= 20',
      approvalActions: [
        { type: 'fieldUpdate', field: 'approval_status', value: 'Director Approved' },
        { type: 'fieldUpdate', field: 'status', value: 'Approved' }
      ],
      rejectionActions: [{ type: 'fieldUpdate', field: 'approval_status', value: 'Rejected' }]
    }
  ],

  finalApprovalActions: [{ type: 'emailAlert', template: 'quote_approved', recipients: ['${owner}'] }],
  finalRejectionActions: [{ type: 'emailAlert', template: 'quote_rejected', recipients: ['${owner}'] }]
});

State Machine

Declarative lifecycle management with states, transitions, guards, and timeouts.

// packages/support/src/case.statemachine.ts
import { StateMachine } from '@objectstack/spec/automation';

export const CaseLifecycle = StateMachine.create({
  name: 'case_lifecycle',
  object: 'case',
  initial: 'new',

  states: [
    {
      name: 'new',
      label: 'New',
      onEntry: [{ type: 'fieldUpdate', field: 'status', value: 'New' }],
      transitions: [
        { to: 'assigned', event: 'assign', guard: 'owner != NULL',
          actions: [{ type: 'emailAlert', template: 'case_assigned', recipients: ['${owner}'] }] }
      ]
    },
    {
      name: 'assigned',
      label: 'Assigned',
      onEntry: [{ type: 'fieldUpdate', field: 'status', value: 'In Progress' }],
      transitions: [
        { to: 'waiting_customer', event: 'request_info' },
        { to: 'escalated', event: 'escalate', guard: 'sla_violation = true OR priority = "Critical"' },
        { to: 'resolved', event: 'resolve' }
      ]
    },
    {
      name: 'waiting_customer',
      label: 'Waiting on Customer',
      timeout: { duration: 72, unit: 'hours', event: 'timeout', to: 'closed' },
      transitions: [{ to: 'assigned', event: 'customer_responded' }]
    },
    {
      name: 'closed',
      label: 'Closed',
      type: 'final',
      onEntry: [
        { type: 'fieldUpdate', field: 'closed_date', value: 'NOW()' },
        { type: 'fieldUpdate', field: 'status', value: 'Closed' }
      ]
    }
  ],

  globalGuards: { hasOwner: 'owner != NULL', isNotClosed: 'status != "Closed"' }
});

AI Metadata

Agent Definition

AI agents combine a system prompt, callable tools, model config, and safety constraints.

// packages/crm/src/sales_assistant.agent.ts
import { Agent } from '@objectstack/spec/ai';

export const SalesAssistant = Agent.create({
  name: 'sales_assistant',
  role: 'Sales AI Assistant',
  description: 'Helps reps with lead qualification, opportunity management, and deal intelligence',

  systemPrompt: `You are an expert sales assistant. Help reps:
1. Qualify leads efficiently
2. Manage opportunities effectively
3. Close deals faster
Always provide actionable insights backed by data.`,

  tools: [
    {
      name: 'scoreLeads',
      description: 'Score leads based on fit, intent, and engagement',
      action: 'lead_scoring',
      parameters: { lead_id: { type: 'string', required: true } }
    },
    {
      name: 'suggestNextSteps',
      description: 'Suggest next best actions for an opportunity',
      action: 'opportunity_next_steps',
      parameters: { opportunity_id: { type: 'string', required: true } }
    },
    {
      name: 'findSimilarDeals',
      description: 'Find similar won deals for insights',
      action: 'deal_intelligence',
      parameters: {
        opportunity_id: { type: 'string', required: true },
        similarity_factors: { type: 'array', items: ['industry', 'deal_size', 'region'] }
      }
    }
  ],

  model: { provider: 'openai', model: 'gpt-4', temperature: 0.7, maxTokens: 2000 },
  memory: { type: 'conversational', maxMessages: 10, summaryThreshold: 8 },
  safety: {
    enableContentFilter: true,
    allowedDomains: ['sales', 'crm', 'customer_data'],
    restrictedActions: ['delete_account', 'transfer_funds']
  }
});

Predictive Model

Declare ML models with feature engineering, training, and deployment settings.

// packages/crm/src/churn_prediction.model.ts
import { PredictiveModel } from '@objectstack/spec/ai';

export const ChurnPredictionModel = PredictiveModel.create({
  name: 'account_churn_prediction',
  description: 'Predict customer churn risk',
  type: 'classification',

  training: {
    algorithm: 'random_forest',
    features: [
      { name: 'account_age_days', type: 'numeric', source: 'DAYS_BETWEEN(created_date, TODAY())' },
      { name: 'total_revenue', type: 'numeric', source: 'SUM(opportunities.amount WHERE stage = "Closed Won")' },
      { name: 'activity_count_30d', type: 'numeric', source: 'COUNT(activities WHERE created_date >= LAST_30_DAYS)' },
      { name: 'industry', type: 'categorical', source: 'industry', encoding: 'one_hot' }
    ],
    target: { name: 'churned', type: 'binary', positiveClass: true,
      definition: 'last_activity_date < LAST_90_DAYS AND status = "Inactive"' },
    hyperparameters: { n_estimators: 100, max_depth: 10, class_weight: 'balanced' },
    dataSource: {
      object: 'account',
      filters: [['type', '=', 'Customer'], ['created_date', '<', 'LAST_YEAR']],
      splitRatio: { train: 0.7, validation: 0.15, test: 0.15 }
    }
  },

  evaluation: {
    metrics: ['accuracy', 'precision', 'recall', 'f1_score', 'auc_roc'],
    thresholds: { min_accuracy: 0.80, min_auc_roc: 0.85 }
  },

  deployment: { endpoint: '/api/ml/predict/churn', batchPrediction: true, realTimePrediction: true },
  monitoring: { trackDrift: true, driftThreshold: 0.05, retrainingSchedule: 'monthly' }
});

Integration Metadata

Webhooks

Outbound webhooks fire on record events with payload templating and retry logic.

// packages/crm/src/opportunity_webhooks.webhook.ts
import { Webhook } from '@objectstack/spec/automation';

export const DealWonWebhook = Webhook.create({
  name: 'deal_won_notification',
  object: 'opportunity',
  event: 'onUpdate',
  condition: 'stage = "Closed Won" AND ISCHANGED(stage)',

  url: 'https://api.company.com/webhooks/deal-won',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': '${env.WEBHOOK_API_KEY}',
    'X-Signature': '${HMAC_SHA256(payload, env.WEBHOOK_SECRET)}'
  },
  payload: {
    event: 'deal.won',
    timestamp: '${NOW()}',
    data: {
      opportunity_id: '${id}', opportunity_name: '${name}', amount: '${amount}',
      account: { id: '${account.id}', name: '${account.name}' },
      owner: { id: '${owner.id}', name: '${owner.name}' }
    }
  },
  retry: { enabled: true, maxAttempts: 3, backoffStrategy: 'exponential', initialDelay: 1000 },
  responseHandling: {
    successCodes: [200, 201, 202],
    onSuccess: [{ type: 'fieldUpdate', field: 'webhook_sent_date', value: 'NOW()' }],
    onFailure: [{ type: 'emailAlert', template: 'webhook_failed', recipients: ['admin@company.com'] }]
  }
});

Connector

Connectors define bi-directional integrations — outbound operations and inbound triggers.

// packages/integrations/src/stripe.connector.ts
import { Connector } from '@objectstack/spec/automation';

export const StripeConnector = Connector.create({
  name: 'stripe_payment',
  category: 'payment',
  label: 'Stripe Payment Gateway',

  authentication: {
    type: 'oauth2',
    authUrl: 'https://connect.stripe.com/oauth/authorize',
    tokenUrl: 'https://connect.stripe.com/oauth/token',
    scopes: ['read_write'],
    clientId: '${env.STRIPE_CLIENT_ID}',
    clientSecret: '${env.STRIPE_CLIENT_SECRET}'
  },

  operations: [{
    name: 'create_customer',
    type: 'create',
    endpoint: { url: 'https://api.stripe.com/v1/customers', method: 'POST' },
    parameters: [
      { name: 'email', type: 'string', required: true, source: '${account.billing_email}' },
      { name: 'name', type: 'string', source: '${account.name}' }
    ],
    responseMapping: { stripe_customer_id: 'id', stripe_created_at: 'created' }
  }],

  triggers: [{
    name: 'payment_succeeded',
    type: 'webhook',
    event: 'payment_intent.succeeded',
    action: {
      type: 'create', object: 'payment',
      fields: { amount: '${data.amount / 100}', status: 'Completed', payment_date: 'NOW()', external_id: '${data.id}' }
    }
  }]
});

Advanced Features

Validation Rules

Formula-based conditions that enforce data quality.

// packages/crm/src/opportunity.validation.ts
import { ValidationRule } from '@objectstack/spec/data';

export const OpportunityValidations = {
  amountRange: ValidationRule.create({
    name: 'opportunity_amount_range', object: 'opportunity', field: 'amount',
    condition: 'amount >= 1000 AND amount <= 10000000',
    errorMessage: 'Opportunity amount must be between $1,000 and $10,000,000',
    active: true
  }),

  closeDateFuture: ValidationRule.create({
    name: 'close_date_future', object: 'opportunity', field: 'close_date',
    condition: 'close_date >= TODAY()',
    errorMessage: 'Close date must be in the future',
    when: 'stage NOT IN ["Closed Won", "Closed Lost"]',
    active: true
  }),

  stageProgression: ValidationRule.create({
    name: 'stage_progression_check', object: 'opportunity',
    condition: `IF(ISCHANGED(stage),
      CASE stage
        WHEN "Qualification" THEN has_decision_maker = true
        WHEN "Proposal" THEN has_budget_confirmed = true AND has_timeline = true
        WHEN "Closed Won" THEN has_signed_contract = true
        ELSE true
      END, true)`,
    errorMessage: 'Stage progression requirements not met',
    active: true
  })
};

Permission Configuration

Permission sets control object-level, field-level, and record-level access.

// packages/core/src/permissions/sales_rep.permission.ts
import { PermissionSet } from '@objectstack/spec/system';

export const SalesRepPermissions = PermissionSet.create({
  name: 'sales_representative',
  label: 'Sales Representative',

  objectPermissions: [
    { object: 'account', create: true, read: true, update: true, delete: false, viewAll: false },
    { object: 'opportunity', create: true, read: true, update: true, delete: false }
  ],

  fieldPermissions: [
    { object: 'opportunity', field: 'discount_percent', read: true, edit: true, constraint: 'value <= 10' },
    { object: 'account', field: 'annual_revenue', read: true, edit: false }
  ],

  recordAccess: {
    account: {
      ownedRecords: true,
      teamRecords: true,
      sharingRules: [{ name: 'territory_based_sharing', condition: 'territory = ${user.territory}' }]
    }
  }
});

Summary

This reference covers the major @objectstack/spec metadata categories:

CategorySpec ModuleFile Suffix
Data (Objects, Fields)@objectstack/spec/data*.object.ts
UI (Pages, Views)@objectstack/spec/ui*.page.ts, *.view.ts
Automation (Workflows, State Machines)@objectstack/spec/automation*.workflow.ts
AI (Agents, Models)@objectstack/spec/ai*.agent.ts, *.model.ts
Integration (Webhooks, Connectors)@objectstack/spec/automation*.webhook.ts, *.connector.ts
System (Permissions, i18n)@objectstack/spec/system*.permission.ts, *.i18n.ts

Next Steps

On this page