Backstage makes building developer portals a breeze — especially with Software Templates for scaffolding services. But what if you want to build dynamic forms?
For example:
“Pick an AWS account, and auto-select the right IAM role or environment based on the account.”
Let’s walk through how I built an AwsAccountPicker — a custom field extension that listens to changes in another field and updates itself accordingly.
I am using FieldProps (from @rjsf/utils) instead of the Backstage-specific FieldExtensionComponentProps
Why FieldProps? While Backstage templates usually encourage using FieldExtensionComponentProps, using FieldProps gives you full access to the underlying JSON Schema Form engine — giving you more control, especially for dynamic behaviours like reacting to other fields.
Requirement
A custom field extension (AwsAccountPicker) that:
- Fetches a list of AWS accounts.
- Observes another form field (like
serviceName) viaformContext.formData. - Dynamically selects the appropriate account based on a pattern match.
Setting Up the Field Extension
In Backstage frontend app/src/components/scaffolder/customScaffolderExtensions.tsx:
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
import { AwsAccountPicker } from './AwsAccountPicker';
export const awsAccountPickerExtension: FieldExtensionOptions = {
name: 'AwsAccountPicker',
component: AwsAccountPicker,
};
Inside app/src/components/scaffolder/AwsAccountPicker.tsx:
import React, { useEffect, useMemo } from 'react';
import { FieldProps } from '@rjsf/utils';
import { TextField, MenuItem } from '@material-ui/core';
export const AwsAccountPicker = ({
formData,
onChange,
uiSchema,
registry,
schema,
}: FieldProps) => {
const allFormData = registry.formContext?.formData ?? {};
const uiOptions = uiSchema['ui:options'] || {};
const accounts = [
{ id: '123456789012', name: 'dev-account' },
{ id: '210987654321', name: 'prod-account' },
]; # Use a hook to get this dynamically from an api
const valueFrom = uiOptions.autofillFrom;
const outputType = uiOptions.output || 'id';
const serviceName = allFormData[valueFrom];
useEffect(() => {
if (!serviceName) return;
const expectedAccount = `${serviceName}-account`;
const match = accounts.find(acc =>
expectedAccount.toLowerCase().includes(acc.name.toLowerCase())
);
if (match && formData !== match[outputType]) {
onChange(match[outputType]);
}
}, [serviceName, formData, onChange, outputType]);
return (
<TextField
select
label={schema.title}
value={formData ?? ''}
onChange={e => onChange(e.target.value)}
>
{accounts.map(account => (
<MenuItem key={account.id} value={account[outputType]}>
{account.name}
</MenuItem>
))}
</TextField>
);
};
Above is a simplified version of the component using FieldProps
How this Works
registry.formContext.formDatagives you access to other form fields.uiSchema['ui:options'].autofillFromdefines which field to observe.useEffecttriggers when the observed field changes and auto-updates the current field.
This approach is clean, does not require extra state management, and works naturally with Backstage scaffolder templates.
Using It in a Template
In template.yaml:
parameters:
- title: Service Details
required:
- serviceName
- awsAccount
properties:
serviceName:
type: string
title: Service Name
awsAccount:
title: AWS Account
type: string
ui:field: AwsAccountPicker
ui:options:
autofillFrom: serviceName
output: id
With this setup, if serviceName = my-service, and your AWS accounts include my-service-account, the form will auto-select the correct account.
The same pattern can be extended to:
- Conditionally fetch data based on other fields.
- Support custom inputs with validation.
- Handle grouped permissions, environments, or region selectors.