Skip to main content

ActionForm

ActionForm renders and submits an OSDK action using @osdk/react-components.

Table of Contents

Import

import { ActionForm } from "@osdk/react-components/experimental";
import type { FormFieldDefinition } from "@osdk/react-components/experimental";

Basic Usage

About @my/osdk and ./client

@my/osdk is a placeholder for your generated SDK package (e.g. @your-app/sdk). ./client (used in scoped object-select examples below) is the file in your app where you exported the OSDK client returned by createClient(...). Replace both with the actual paths in your project.

import { updateEmployee } from "@my/osdk";
import { ActionForm } from "@osdk/react-components/experimental";

function UpdateEmployeeForm() {
return <ActionForm actionDefinition={updateEmployee} />;
}

ActionForm fetches action metadata, renders fields for the action parameters, runs client-side validation, and calls the OSDK action when the user submits. The form title is hidden by default. Pass showFormTitle={true} to show it; the title uses formTitle when provided, otherwise the action display name, otherwise the action API name.

Choosing ActionForm vs BaseForm

Use caseUse this API
Render a form directly from an OSDK action and submit with applyActionActionForm
Override generated fields for an OSDK actionActionForm with formFieldDefinitions
Build a form from manually-authored field definitionsBaseForm
Compose sections, standalone fields, or custom form layoutsBaseForm

Default Field Rendering

When formFieldDefinitions is omitted, ActionForm derives one field per action parameter from action metadata.

Action parameter typeDefault field componentDefault behavior
stringTEXT_INPUTSingle-line text input
booleanRADIO_BUTTONSTrue/False radio options
integer, double, longNUMBER_INPUTNumeric input
datetime, timestampDATETIME_PICKERDate picker
attachment, mediaReferenceFILE_PICKERFile picker
{ type: "object" }OBJECT_SELECTObject selector for the referenced object type
{ type: "objectSet" }OBJECT_SETRead-only object set summary
marking, geohash, geoshape, objectTypeUNSUPPORTEDDisabled field recommending a custom field
{ type: "interface" }, { type: "struct" }UNSUPPORTEDDisabled field recommending a custom field

Required validation is inferred from the action parameter's nullability. Additional client-side validation comes from field-specific props such as min, max, minLength, maxLength, and maxSize.

Unsupported Features

ActionForm is a lightweight OSDK component and does not yet match the full ActionForm in Foundry. The table below lists the main gaps to account for when adopting it.

FeatureCurrent behaviorWorkaround
Backend action validationThe form does not call backend validation before submit and does not process validation-derived displays, allowed values, defaults, or section results.Use formFieldDefinitions validation props, field-level validate, onSubmit, and onError for app-owned checks and submission error handling.
Unsupported generated parameter typesmarking, geohash, geoshape, objectType, interface, and struct render as disabled unsupported fields by default.Override those parameters with fieldComponent: "CUSTOM" or another compatible component in formFieldDefinitions.
Action-authored layout metadataAction-defined sections, form content ordering, section validation, and layout toggles are not read from metadata. Fields render in metadata parameter order unless fully replaced by formFieldDefinitions.Use BaseForm with explicit FormContentItem sections when you need custom grouping, or control field order with formFieldDefinitions.
Conditional logic and dynamic display stateBackend-driven hidden, disabled, required, and allowed-value rules are not evaluated as the user edits the form.Encode static display state in field definitions, or manage dynamic state in your app and pass updated formFieldDefinitions / controlled formState.
Defaults and prefillsBackend prefills, type-class defaults, current timestamp defaults, and validation-derived default values are not applied automatically.Provide defaultValue in field definitions, seed controlled formState, or compute defaults in app code before rendering.

Custom Field Definitions

Use formFieldDefinitions when the default rendering for a parameter is not the UI you want. The list is a complete replacement: include every parameter that should appear in the form.

const fields = [
{
fieldKey: "fullName",
label: "Full name",
fieldComponent: "TEXT_INPUT",
fieldComponentProps: {
placeholder: "Jane Doe",
minLength: 2,
},
},
{
fieldKey: "yearsExperience",
label: "Years of experience",
fieldComponent: "NUMBER_INPUT",
fieldComponentProps: {
min: 0,
max: 80,
},
},
{
fieldKey: "isRemote",
label: "Remote employee",
fieldComponent: "SWITCH",
fieldComponentProps: {},
},
] satisfies Array<FormFieldDefinition<typeof updateEmployee>>;

<ActionForm actionDefinition={updateEmployee} formFieldDefinitions={fields} />;

Rich dropdown labels

Use itemToStringLabel for the dropdown's text behavior: search matching, accessibility labels, fallback item keys, and default visual text. Add renderItemLabel when the visible label needs richer React content while preserving the same string behavior.

const fields = [
{
fieldKey: "assigneeUserId",
label: "Assignee",
fieldComponent: "DROPDOWN",
fieldComponentProps: {
items: userIds,
itemToStringLabel: (userId) => userNames[userId] ?? userId,
renderItemLabel: (userId) => (
<span>
<strong>{userNames[userId] ?? userId}</strong>
<span>{userTeams[userId]}</span>
</span>
),
},
},
] satisfies Array<FormFieldDefinition<typeof updateEmployee>>;

Scoped object select fields

OBJECT_SELECT can load options from either an object type or a pre-scoped object set. Pass objectType for an unfiltered selector, or pass objectSet to limit selectable options. The two are mutually exclusive. Search text is applied within the object set, and the current value is not automatically cleared when it is outside that set.

import { Employee, updateEmployee } from "@my/osdk";
import { useMemo } from "react";
import client from "./client";

function UpdateEmployeeForm() {
const marketingEmployees = useMemo(
() => client(Employee).where({ department: "Marketing" }),
[],
);

const fields = [
{
fieldKey: "manager",
label: "Marketing manager",
fieldComponent: "OBJECT_SELECT",
fieldComponentProps: {
objectSet: marketingEmployees,
placeholder: "Search Marketing employees…",
},
},
] satisfies Array<FormFieldDefinition<typeof updateEmployee>>;

return (
<ActionForm
actionDefinition={updateEmployee}
formFieldDefinitions={fields}
/>
);
}

For a completely custom field, use fieldComponent: "CUSTOM" and provide customRenderer:

const fields = [
{
fieldKey: "approvalReason",
label: "Approval reason",
fieldComponent: "CUSTOM",
fieldComponentProps: {
customRenderer: ({ value, onChange }) => (
<textarea
value={value != null ? String(value) : ""}
onChange={(event) => onChange?.(event.target.value)}
/>
),
},
},
] satisfies Array<FormFieldDefinition<typeof approveEmployee>>;

<ActionForm actionDefinition={approveEmployee} formFieldDefinitions={fields} />;

Behavior Notes

  • formFieldDefinitions replaces generated fields. Passing one custom field means only that field renders.
  • fieldKey must match the action parameter key; submitted form state uses the same keys.
  • fieldComponentProps should not include value, onChange, or id; form state wiring provides those.
  • Controlled mode requires both formState and onFormStateChange.
  • Use onSubmit for custom submission behavior. If onSubmit is omitted, ActionForm calls applyAction and then onSuccess.

Examples

Controlled form state

const [formState, setFormState] = useState({
fullName: "Ada Lovelace",
isRemote: true,
});

<ActionForm
actionDefinition={updateEmployee}
formState={formState}
onFormStateChange={setFormState}
/>;

Custom submit handling

<ActionForm
actionDefinition={updateEmployee}
onSubmit={async (formState, applyAction) => {
await applyAction(formState);
showToast("Employee updated");
}}
/>;

Styling

ActionForm is built on BaseForm, so the form layout and field styling use the same CSS variables documented in CSSVariables.md.

ActionForm does not add outer padding by default. Apply internal spacing by setting the form padding variables on the form or a wrapper element:

.employeeForm {
--osdk-form-content-padding-inline: calc(var(--osdk-surface-spacing) * 4);
--osdk-form-content-padding-block: calc(var(--osdk-surface-spacing) * 4);
}
<div className="employeeForm">
<ActionForm actionDefinition={updateEmployee} />
</div>;

Use --osdk-form-content-padding-inline to apply shared horizontal padding to the header, fields, and footer. Use --osdk-form-content-padding-block to control vertical padding for the form fields section. For card-style outer spacing, apply margin, border, background, and border radius to the wrapper rather than the form itself.