ArchitectureControls

Object Control

Object controls provide a JSON editor for supplying structured data — objects, arrays, or any JSON-serializable value. They're ideal for HTTP headers, custom metadata, configuration maps, and typed arrays where users need to supply composite values rather than a single scalar.

Basic Usage

import * as i from '@xentom/integration-framework';

// Plain object input
i.controls.object({
  label: 'Custom Headers',
  placeholder: '{ "X-Custom-Header": "value" }',
});

// With a default value
i.controls.object({
  label: 'Default Config',
  default: { retries: 3, timeout: 5000 },
  rows: 6,
});

Configuration Options

Prop

Type

Important Note

Object controls parse their value as JSON, not as JavaScript. The editor accepts any valid JSON literal — objects, arrays, strings, numbers, booleans, and null — but does not evaluate expressions or execute code. If you need evaluated JavaScript, use an Expression Control instead.

Typed Arrays

ObjectControlBuilder is generic, so you can constrain the expected value type at the call site. The type parameter flows through to default and the rest of the integration's type system:

// Typed string array
tags: i.pins.data<string[]>({
  control: i.controls.object<string[]>({
    label: 'Tags',
    description: 'List of tags to attach to the record',
    default: [],
  }),
});

// Typed object array
recipients: i.pins.data<{ email: string; name: string }[]>({
  control: i.controls.object<{ email: string; name: string }[]>({
    label: 'Recipients',
    default: [{ email: '[email protected]', name: 'User' }],
    rows: 6,
  }),
});

Common Use Cases

HTTP Headers

headers: i.pins.data({
  control: i.controls.object({
    label: 'HTTP Headers',
    description: 'Additional headers to include with every request',
    placeholder: '{\n  "X-Api-Version": "2",\n  "Accept-Language": "en-US"\n}',
    default: {},
    rows: 6,
  }),
});

Custom Metadata

metadata: i.pins.data({
  control: i.controls.object({
    label: 'Metadata',
    description: 'Arbitrary key-value pairs attached to the record',
    default: {},
    rows: 4,
  }),
});

Tag Lists

tags: i.pins.data<string[]>({
  control: i.controls.object<string[]>({
    label: 'Tags',
    description: 'List of tags, e.g. ["billing", "urgent"]',
    default: [],
  }),
});

Nested Configuration

// Environment variable for a complex config block
SMTP_OPTIONS: i.env({
  control: i.controls.object({
    label: 'SMTP Options',
    description: 'Additional options passed directly to the SMTP client',
    default: { pool: true, maxConnections: 5 },
    rows: 8,
  }),
});

Usage in Nodes

Object controls work in both environment variables and node pins:

export default i.integration({
  env: {
    // Global object setting shared across all nodes
    DEFAULT_HEADERS: i.env({
      control: i.controls.object({
        label: 'Default Headers',
        description: 'Headers included in every outgoing request',
        default: { 'Content-Type': 'application/json' },
        rows: 6,
      }),
    }),
  },

  nodes: {
    sendRequest: i.nodes.action({
      inputs: {
        // Per-node object inputs
        body: i.pins.data({
          control: i.controls.object({
            label: 'Request Body',
            description: 'JSON body to send with the request',
            placeholder: '{\n  "key": "value"\n}',
            rows: 10,
          }),
        }),
        extraHeaders: i.pins.data({
          control: i.controls.object({
            label: 'Extra Headers',
            description: 'Additional headers merged with the defaults',
            default: {},
            rows: 4,
          }),
        }),
      },

      outputs: {
        response: i.pins.data(),
        statusCode: i.pins.data(),
      },

      async run({ inputs, env, next }) {
        const headers = {
          ...env.DEFAULT_HEADERS,
          ...inputs.extraHeaders,
        };

        const response = await fetch(apiUrl, {
          method: 'POST',
          headers,
          body: JSON.stringify(inputs.body),
        });

        next({
          response: await response.json(),
          statusCode: response.status,
        });
      },
    }),
  },
});

Best Practices

Provide a Meaningful Placeholder

Show users what a valid value looks like directly in the placeholder — especially helpful for nested structures:

// Good - placeholder shows the expected shape
connectionOptions: i.pins.data({
  control: i.controls.object({
    label: 'Connection Options',
    placeholder: '{\n  "ssl": true,\n  "timeout": 5000\n}',
  }),
});

// Avoid - placeholder gives no structural hint
connectionOptions: i.pins.data({
  control: i.controls.object({
    label: 'Connection Options',
    placeholder: 'Enter JSON...', // Not helpful
  }),
});

Supply a Default Value

When a sensible empty state exists, set it as the default so users can see the expected type at a glance:

// Empty object as default — signals "add your key-value pairs here"
queryParams: i.pins.data({
  control: i.controls.object({
    label: 'Query Parameters',
    default: {},
  }),
});

// Empty array as default — signals "add items to this list"
allowedIps: i.pins.data<string[]>({
  control: i.controls.object<string[]>({
    label: 'Allowed IPs',
    default: [],
  }),
});

Adjust Rows for Expected Complexity

Use rows to give users enough vertical space without overwhelming the form:

// Small objects — fewer rows
labels: i.pins.data({
  control: i.controls.object({
    label: 'Labels',
    rows: 3,
  }),
});

// Deeply nested or large objects — more rows
policyDocument: i.pins.data({
  control: i.controls.object({
    label: 'Policy Document',
    description: 'Full IAM policy document in JSON format',
    rows: 20,
  }),
});

Prefer Expression Control for Dynamic Values

If users need to compute the object at runtime rather than supply a static JSON literal, use an Expression Control instead:

// Static structure known at configuration time → Object Control
staticConfig: i.pins.data({
  control: i.controls.object({
    label: 'Config',
    default: { feature: true },
  }),
});

// Structure depends on runtime data → Expression Control
dynamicConfig: i.pins.data({
  control: i.controls.expression({
    label: 'Config Builder',
    placeholder: '({ userId }) => ({ feature: userId !== null })',
  }),
});

On this page