> ## Documentation Index
> Fetch the complete documentation index at: https://docs.elementum.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Upcoming Features

> See what we're working on at Elementum

export const EmailSubscriptionPicker = ({webhookUrl, logUrl = "https://script.google.com/macros/s/AKfycbwYKNyp9YTtV7fhwZOKwePB-0_cOz8jOD1kBLEprmvbTD5LBPn_iSuYagvaWlxbmtg/exec", heading = "Subscribe to email updates", description, buttonLabel = "Subscribe", lists = [{
  key: "ga-releases",
  label: "General Availability Release notifications",
  description: "Get an email when a new release ships — about twice a month."
}, {
  key: "upcoming-features",
  label: "Upcoming Feature updates",
  description: "Hear about new Beta features being tested — expect a few updates each week."
}]}) => {
  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  const MAX_TEXT_LENGTH = 120;
  const MODEL_DEPRECATIONS_KEY = "model-deprecations";
  const hasDeprecationsList = lists.some(list => list.key === MODEL_DEPRECATIONS_KEY);
  const subscribeLists = hasDeprecationsList && lists.length > 1 ? lists.filter(list => list.key !== MODEL_DEPRECATIONS_KEY) : lists;
  const logSubmissionAttempt = payload => {
    if (!logUrl) return;
    try {
      fetch(logUrl, {
        method: "POST",
        mode: "no-cors",
        keepalive: true,
        headers: {
          "Content-Type": "text/plain;charset=UTF-8"
        },
        body: JSON.stringify(payload)
      }).catch(() => {});
    } catch (e) {}
  };
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [company, setCompany] = useState("");
  const [email, setEmail] = useState("");
  const [website, setWebsite] = useState("");
  const [selectedLists, setSelectedLists] = useState(() => subscribeLists.map(l => l.key));
  const [status, setStatus] = useState("idle");
  const [errorMessage, setErrorMessage] = useState("");
  const isSubmitting = status === "loading";
  const [uid] = useState(() => `esp-${Math.random().toString(36).slice(2, 9)}`);
  const firstNameId = `${uid}-first-name`;
  const lastNameId = `${uid}-last-name`;
  const companyId = `${uid}-company`;
  const emailId = `${uid}-email`;
  const websiteId = `${uid}-website`;
  const statusId = `${uid}-status`;
  const toggleList = key => {
    setSelectedLists(prev => prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]);
  };
  const handleSubmit = async event => {
    event.preventDefault();
    setErrorMessage("");
    if (website.trim().length > 0) {
      setStatus("success");
      setFirstName("");
      setLastName("");
      setCompany("");
      setEmail("");
      return;
    }
    const trimmedFirstName = firstName.trim();
    const trimmedLastName = lastName.trim();
    const trimmedCompany = company.trim();
    const trimmedEmail = email.trim();
    if (!trimmedFirstName || trimmedFirstName.length > MAX_TEXT_LENGTH) {
      setStatus("error");
      setErrorMessage("Please enter your first name (up to 120 characters).");
      return;
    }
    if (!trimmedLastName || trimmedLastName.length > MAX_TEXT_LENGTH) {
      setStatus("error");
      setErrorMessage("Please enter your last name (up to 120 characters).");
      return;
    }
    if (!trimmedCompany || trimmedCompany.length > MAX_TEXT_LENGTH) {
      setStatus("error");
      setErrorMessage("Please enter your company (up to 120 characters).");
      return;
    }
    if (!EMAIL_REGEX.test(trimmedEmail)) {
      setStatus("error");
      setErrorMessage("Please enter a valid email address.");
      return;
    }
    if (selectedLists.length === 0) {
      setStatus("error");
      setErrorMessage("Select at least one list to subscribe to.");
      return;
    }
    if (!webhookUrl) {
      setStatus("error");
      setErrorMessage("This form is not configured yet. Please try again later.");
      return;
    }
    setStatus("loading");
    const payload = {
      firstName: trimmedFirstName,
      lastName: trimmedLastName,
      company: trimmedCompany,
      email: trimmedEmail,
      lists: selectedLists.filter(key => subscribeLists.some(list => list.key === key)),
      action: "subscribe",
      source: typeof window !== "undefined" ? window.location.pathname : "",
      submittedAt: new Date().toISOString()
    };
    logSubmissionAttempt(payload);
    try {
      const response = await fetch(webhookUrl, {
        method: "POST",
        mode: "no-cors",
        headers: {
          "Content-Type": "text/plain;charset=UTF-8"
        },
        body: JSON.stringify(payload)
      });
      if (response.type !== "opaque" && !response.ok) {
        throw new Error(`Request failed with status ${response.status}`);
      }
      setStatus("success");
      setFirstName("");
      setLastName("");
      setCompany("");
      setEmail("");
      setSelectedLists(subscribeLists.map(l => l.key));
    } catch (err) {
      setStatus("error");
      setErrorMessage("We couldn't complete your subscription right now. Please try again in a moment.");
    }
  };
  const inputClass = "w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:placeholder:text-zinc-500";
  const labelClass = "block text-sm font-medium text-zinc-700 dark:text-zinc-200 mb-1";
  const checkboxLabelClass = "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-200";
  const checkboxClass = "mt-0.5 h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-600/30 dark:border-zinc-700 dark:bg-zinc-900";
  return <div className="email-form not-prose my-4 rounded-xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900/40">
      <h3 className="m-0 text-base font-semibold text-zinc-900 dark:text-zinc-50">
        {heading}
      </h3>
      {description ? <p className="mt-1 mb-0 text-sm text-zinc-600 dark:text-zinc-300">{description}</p> : null}

      <form onSubmit={handleSubmit} noValidate className="mt-4 flex flex-col gap-3">
        <div className="grid gap-3 md:grid-cols-2">
          <div>
            <label htmlFor={firstNameId} className={labelClass}>
              First name
            </label>
            <input id={firstNameId} type="text" name="firstName" autoComplete="given-name" required maxLength={MAX_TEXT_LENGTH} value={firstName} onChange={e => setFirstName(e.target.value)} disabled={isSubmitting} className={inputClass} placeholder="Jane" />
          </div>
          <div>
            <label htmlFor={lastNameId} className={labelClass}>
              Last name
            </label>
            <input id={lastNameId} type="text" name="lastName" autoComplete="family-name" required maxLength={MAX_TEXT_LENGTH} value={lastName} onChange={e => setLastName(e.target.value)} disabled={isSubmitting} className={inputClass} placeholder="Doe" />
          </div>
        </div>

        <div className="grid gap-3 md:grid-cols-2">
          <div>
            <label htmlFor={companyId} className={labelClass}>
              Company
            </label>
            <input id={companyId} type="text" name="company" autoComplete="organization" required maxLength={MAX_TEXT_LENGTH} value={company} onChange={e => setCompany(e.target.value)} disabled={isSubmitting} className={inputClass} placeholder="Acme Corp" />
          </div>
          <div>
            <label htmlFor={emailId} className={labelClass}>
              Work email
            </label>
            <input id={emailId} type="email" name="email" autoComplete="email" required value={email} onChange={e => setEmail(e.target.value)} disabled={isSubmitting} className={inputClass} placeholder="jane@acme.com" />
          </div>
        </div>

        {subscribeLists.length > 1 ? <fieldset className="mt-1 flex flex-col gap-2">
            <legend className={labelClass}>Subscribe me to:</legend>
            {subscribeLists.map(list => {
    const checkboxId = `${uid}-list-${list.key}`;
    return <label key={list.key} htmlFor={checkboxId} className={checkboxLabelClass}>
                  <input id={checkboxId} type="checkbox" name="lists" value={list.key} checked={selectedLists.includes(list.key)} onChange={() => toggleList(list.key)} disabled={isSubmitting} className={checkboxClass} />
                  <span className="flex flex-col">
                    <span className="font-medium">{list.label}</span>
                    {list.description ? <span className="text-zinc-500 dark:text-zinc-400">{list.description}</span> : null}
                  </span>
                </label>;
  })}
          </fieldset> : null}

        <div aria-hidden="true" style={{
    position: "absolute",
    left: "-10000px",
    top: "auto",
    width: "1px",
    height: "1px",
    overflow: "hidden"
  }}>
          <label htmlFor={websiteId}>Website (leave blank)</label>
          <input id={websiteId} type="text" name="website" tabIndex={-1} autoComplete="off" value={website} onChange={e => setWebsite(e.target.value)} />
        </div>

        <button type="submit" disabled={isSubmitting} className="mt-1 inline-flex w-full items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-blue-500 dark:hover:bg-blue-400">
          {isSubmitting ? "Subscribing…" : buttonLabel}
        </button>

        <div id={statusId} role="status" aria-live="polite" className="min-h-[1.25rem] text-sm">
          {status === "success" ? <span className="text-emerald-700 dark:text-emerald-400">
              Thanks — your subscription was submitted.
            </span> : null}
          {status === "error" ? <span className="text-red-700 dark:text-red-400">{errorMessage}</span> : null}
        </div>
      </form>

      <p className="mt-3 mb-0 text-xs text-zinc-500 dark:text-zinc-400">
        Already subscribed?{" "}
        <a href="/release-notes/unsubscribe" className="underline hover:text-zinc-700 dark:hover:text-zinc-200">
          Unsubscribe
        </a>
        .
      </p>
    </div>;
};

We're always trying to improve our platform for our customers. Here, you can get an inside look at what's currently in beta at Elementum.

Remember, this list is provided as a heads-up—not as an invitation to test or a promise of what capabilities may be included. Most customers will not see these features until general availability. For what's available today, see the [most recent release notes](/release-notes/august-2026).

## What to expect

<Info>
  **Heads-up, not a commitment.** The information below explains how we treat beta features and why delivery and availability can change.
</Info>

* **Currently being tested.** These features are in active development and testing. Behavior and scope may change.
* **No guaranteed delivery date.** We do not commit to a specific release date for any feature listed here.
* **No guarantee of delivery.** Priorities shift based on resources and customer needs. A feature in beta may be delayed, changed, or not released to General Availability.
* **Limited availability.** Some features may be tested by select customers before being released to General Availability. Broader access is not guaranteed until a feature is generally available.
* **Labs badge.** Experimental features available in the platform but still being validated are marked with an orange **Labs** badge in the Elementum UI. Treat these the same way—behavior, scope, and availability may change before General Availability.

## Features in beta

This list is updated as new beta features become available to give you an idea of upcoming improvements likely to occur in Elementum. The colored badges indicate the area of Elementum where you will see the update.

<Update label="Automations: Search Table Records Action" rss={{ title: "Automations: Search Table Records Action - August 28, 2026", description: "A new Search Table Records action in the Automations action picker lets an automation look up data directly from a table instead of an app. Automation builders can source records from tables to drive downstream steps, expanding beyond app-only lookups." }}>
  ### Automations: Search Table Records Action <Badge color="yellow" size="sm">Automations</Badge>

  A new Search Table Records action in the Automations action picker lets an automation look up data directly from a table instead of an app. Automation builders can source records from tables to drive downstream steps, expanding beyond app-only lookups.
</Update>

<Update label="Automations: Run In Parallel Operator" rss={{ title: "Automations: Run In Parallel Operator - August 28, 2026", description: "A Run In Parallel operator in the Automation Builder lets admins define multiple branches of tasks that execute simultaneously. Running branches at the same time reduces end-to-end runtime for automation steps that do not need to happen in sequence." }}>
  ### Automations: Run In Parallel Operator <Badge color="yellow" size="sm">Automations</Badge>

  A Run In Parallel operator in the Automation Builder lets admins define multiple branches of tasks that execute simultaneously. Running branches at the same time reduces end-to-end runtime for automation steps that do not need to happen in sequence.
</Update>

<Update label="Roll-Up Widget" rss={{ title: "Roll-Up Widget - August 27, 2026", description: "App administrators can configure a Roll-Up widget with a top-level filter and a custom hierarchy of tables or aspects, choosing which measures roll up and how they sum at each level. Reviewers see the summed total at the top and can drill down one level at a time to the underlying detail records driving that total." }}>
  ### Roll-Up Widget <Badge color="orange" size="sm">Apps</Badge>

  App administrators can configure a Roll-Up widget with a top-level filter and a custom hierarchy of tables or aspects, choosing which measures roll up and how they sum at each level. Reviewers see the summed total at the top and can drill down one level at a time to the underlying detail records driving that total.
</Update>

<Update label="Data Entry View" rss={{ title: "Data Entry View - August 27, 2026", description: "App administrators can configure a no-code Data Entry view that turns any app into an editable grid, choosing which fields are editable and how rows are filtered, sorted, and grouped. Users type or paste values directly into cells, save all edits at once, and see per-line and roll-up totals recalculate live as they work." }}>
  ### Data Entry View <Badge color="orange" size="sm">Apps</Badge>

  App administrators can configure a no-code Data Entry view that turns any app into an editable grid, choosing which fields are editable and how rows are filtered, sorted, and grouped. Users type or paste values directly into cells, save all edits at once, and see per-line and roll-up totals recalculate live as they work.
</Update>

<Update label="Dynamic Approvals: SLAs" rss={{ title: "Dynamic Approvals: SLAs - August 26, 2026", description: "Dynamic Approvals can be configured with an SLA policy that sets targets for each step in the process and sends alert emails to approvers at a configurable time before the deadline. When an SLA breaches, the approval workflow can branch on expiration to determine what happens next." }}>
  ### Dynamic Approvals: SLAs <Badge color="orange" size="sm">Apps</Badge>

  Dynamic Approvals can be configured with an SLA policy that sets targets for each step in the process and sends alert emails to approvers at a configurable time before the deadline. When an SLA breaches, the approval workflow can branch on expiration to determine what happens next.
</Update>

<Update label="Deployments Overview" rss={{ title: "Deployments Overview - August 20, 2026", description: "Preview a deployment with a dry run that walks the full process without writing any changes, showing every App, Element, Task, and Table in scope along with the admins responsible for each object. Reopen any completed run—dry run or real deployment—to see exactly what it covered, turning deployment history into an audit trail." }}>
  ### Deployments Overview <Badge color="purple" size="sm">Org Settings</Badge>

  Preview a deployment with a dry run that walks the full process without writing any changes, showing every App, Element, Task, and Table in scope along with the admins responsible for each object. Reopen any completed run—dry run or real deployment—to see exactly what it covered, turning deployment history into an audit trail.
</Update>

<Update label="Record Work Breakdown" rss={{ title: "Record Work Breakdown - August 20, 2026", description: "Every automation action on a record is categorized as AI, Computer, or Human Action, showing at a glance how much of a record's work ran autonomously versus manually. A work breakdown bar with a legend sits atop the Automations and AI panel, with hover details for exact percentages and action counts." }}>
  ### Record Work Breakdown <Badge color="orange" size="sm">Record Details</Badge>

  Every automation action on a record is categorized as AI, Computer, or Human Action, showing at a glance how much of a record's work ran autonomously versus manually. A work breakdown bar with a legend sits atop the Automations and AI panel, with hover details for exact percentages and action counts.
</Update>

<Update label="Agent Managed View Customization" rss={{ title: "Agent Managed View Customization - August 18, 2026", description: "App administrators can brand and customize the full-page agent chat experience with a custom image, header, and sub-header on any agent managed view. A custom view can also render structured content alongside the chat, with schema validation built in." }}>
  ### Agent Managed View Customization <Badge color="blue" size="sm">Intelligence</Badge>

  App administrators can brand and customize the full-page agent chat experience with a custom image, header, and sub-header on any agent managed view. A custom view can also render structured content alongside the chat, with schema validation built in.
</Update>

<Update label="Dynamic Approvals: Segregation of Duties" rss={{ title: "Dynamic Approvals: Segregation of Duties - August 18, 2026", description: "Built-in compliance rules for Dynamic Approvals prevent the person who requested an approval from approving it, and prevent any single user from appearing more than once in the same approval chain. These rules help enforce segregation-of-duties requirements across approval workflows without additional configuration." }}>
  ### Dynamic Approvals: Segregation of Duties <Badge color="orange" size="sm">Apps</Badge>

  Built-in compliance rules for Dynamic Approvals prevent the person who requested an approval from approving it, and prevent any single user from appearing more than once in the same approval chain. These rules help enforce segregation-of-duties requirements across approval workflows without additional configuration.
</Update>

<Update label="Dynamic Approvals: Workflow Inspector" rss={{ title: "Dynamic Approvals: Workflow Inspector - August 4, 2026", description: "Clicking an inflight approval opens a side panel with the full details of the approval process, including the current step, the assigned approvers, and progress so far. The panel is available from the Record Details page, the My Approvals page, and the approvals list on a record." }}>
  ### Dynamic Approvals: Workflow Inspector <Badge color="orange" size="sm">Apps</Badge>

  Clicking an inflight approval opens a side panel with the full details of the approval process, including the current step, the assigned approvers, and progress so far. The panel is available from the Record Details page, the My Approvals page, and the approvals list on a record.
</Update>

<Update label="Dynamic Approvals: Expiration Outcomes" rss={{ title: "Dynamic Approvals: Expiration Outcomes - July 29, 2026", description: "A new approval-process-level setting decides what happens when a step in a Dynamic Approval process fails to complete: reject, cancel, approve, or hold open. Approval processes conclude in a predetermined way instead of stalling in a pending state when a step expires." }}>
  ### Dynamic Approvals: Expiration Outcomes <Badge color="orange" size="sm">Apps</Badge>

  A new approval-process-level setting decides what happens when a step in a Dynamic Approval process fails to complete: reject, cancel, approve, or hold open. Approval processes conclude in a predetermined way instead of stalling in a pending state when a step expires.
</Update>

<Update label="Dynamic Approvals" rss={{ title: "Dynamic Approvals - July 29, 2026", description: "A new Dynamic Approvals approval type lets app administrators route each request to the right user or group based on rules and record data, rather than hard-coding approver IDs. Chains can span multiple sequential steps with concurrent approvers at each step, and per-rule filters decide which approvers apply so approval processes adapt to the record instead of getting stuck." }}>
  ### Dynamic Approvals <Badge color="orange" size="sm">Apps</Badge>

  A new Dynamic Approvals approval type lets app administrators route each request to the right user or group based on rules and record data, rather than hard-coding approver IDs. Chains can span multiple sequential steps with concurrent approvers at each step, and per-rule filters decide which approvers apply so approval processes adapt to the record instead of getting stuck.

  <AccordionGroup>
    <Accordion title="Context & configuration">
      **Who can configure it:** App Administrators.

      **Where to find it:** **App** → **Workflows & Processes** → **Approval Processes**.

      **Configuration steps:**

      1. Open your App and go to **Workflows & Processes** → **Approval Processes** in the left navigation menu.
      2. Click **Create New** and choose the **Dynamic Approval** option.
      3. Enter a name for the approval process and click **Save**.
      4. Use the **Table filter columns** dropdown to select the fields the approvals will be filtered on.
      5. Click **Add Step** to build the approval as it progresses. For each step, configure the approver type, the users, and the preconditions that must be met, then save your changes for that step.
      6. Repeat the previous step for each step the approval should follow.
      7. Select **Settings** in the top-right corner to configure the approval name, expiration outcome, delegation depth, whether the approval can be started manually, locked fields, and SLAs.

      **Behavior notes:**

      * Steps run in the order you add them, and each step can have concurrent approvers who act at the same time.
      * Save your changes for each step individually before moving on to the next one.
      * Per-step preconditions decide whether a step applies, so the approval process adapts to the record instead of stalling on approvers who aren't relevant.
      * The **Settings** panel controls process-level options, including the expiration outcome that determines what happens when a step fails to complete, delegation depth, manual start, locked fields, and SLAs.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="Voice Call Handoff" rss={{ title: "Voice Call Handoff - July 24, 2026", description: "Voice agents can transfer a live call to a human or another destination without dropping the caller or losing context. Configure a Handoff tool on any voice agent with a SIP endpoint as the destination, and the agent decides when to trigger the transfer based on the conversation." }}>
  ### Voice Call Handoff <Badge color="blue" size="sm">Intelligence</Badge>

  Voice agents can transfer a live call to a human or another destination without dropping the caller or losing context. Configure a Handoff tool on any voice agent with a SIP endpoint as the destination, and the agent decides when to trigger the transfer based on the conversation.
</Update>

<Update label="Add Option from List View" rss={{ title: "Add Option from List View - July 15, 2026", description: "Add a missing option to a dynamic picklist directly from your List view, without leaving the list, opening a separate form, or navigating to the underlying element. Users stay in their data-entry flow, and Elementum handles the setup so any new record automatically qualifies for the column it was created from." }}>
  ### Add Option from List View <Badge color="green" size="sm">Workspace</Badge>

  Add a missing option to a dynamic picklist directly from your List view, without leaving the list, opening a separate form, or navigating to the underlying element. Users stay in their data-entry flow, and Elementum handles the setup so any new record automatically qualifies for the column it was created from.

  <AccordionGroup>
    <Accordion title="Context & configuration">
      **Who can configure it:** All Users with permission to create records in the element referenced by the dynamic picklist column.

      **Where to find it:** Any **List view** with a dynamic picklist column.

      **Configuration steps:**

      1. In the **List view**, click a dynamic picklist cell.
      2. Begin typing a new picklist option.
      3. Press **Enter**.
      4. Fill out the required fields in the **Create** modal to complete the record creation process.

      **Behavior notes:**

      * The **Create** modal opens with the record name pre-filled from what you typed, so you only need to complete the remaining required fields.
      * For picklists with simple equals filters, the matching field is pre-filled and locked in the **Create** modal so the new record automatically satisfies the column's criteria.
      * The new record is created in the element the dynamic picklist references, not the element backing the current List view.
      * Once the record is saved, it becomes a selectable option in every other dynamic picklist that references the same element.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="cXML Webhook Trigger" rss={{ title: "cXML Webhook Trigger - July 10, 2026", description: "You can now create a cXML Webhook Integration in Company Settings with the credentials required for cXML invoice event types, then use it as a trigger in automations. The trigger supports the synchronous responses that cXML requests require." }}>
  ### cXML Webhook Trigger <Badge color="yellow" size="sm">Automations</Badge>

  You can now create a cXML Webhook Integration in Company Settings with the credentials required for cXML invoice event types, then use it as a trigger in automations. The trigger supports the synchronous responses that cXML requests require.
</Update>

## Closed beta

These features are available to a limited set of customers by invitation and are not broadly available yet.

* **Elementum Development Kit (EDK)** — A code-first toolkit for building Elementum in TypeScript as an alternative to building in the platform. Represent Apps, Elements, Automations, Agents, Skills, Flows, Search Tables, and Approval Chains as typed source, then follow a Terraform-like lifecycle: pull existing entities into a local workspace, author changes with typed `@catalog` references, preview a structural diff with `plan`, and apply approved changes. Packaged coding-agent playbooks teach AI coding agents the EDK's conventions, so you can describe an outcome in plain language and delegate the authoring from your editor or CLI. Get started with the [EDK documentation](/edk/getting-started) to install the toolchain, authenticate, and bootstrap an organization workspace.

## Stay in the loop

<EmailSubscriptionPicker webhookUrl="https://elementum.elementum.io/api/v1/webhooks/38fad8b3-0323-44ba-be52-d622b1a29289" heading="Subscribe to release email updates" description="Choose what you'd like to hear about. Both lists are selected by default." />
