Skip to main content

Schema & dynamic fields

A node's schema() (and advanced() / connection()) returns an array of field definitions built with the fluent Schema API. These describe the node's config form — for the interactive builder, and for any UI built on top of the registry.

use Qanna\WorkflowEngine\Engine\Schema\Schema;

public static function schema(): array
{
return [
Schema::text('url')->required()->placeholder('https://…')->help('Supports {{ }}.'),
Schema::select('method')
->options(['get' => 'GET', 'post' => 'POST'])
->default('get'),
Schema::json('body')->when('method', ['post']),
];
}

Field types

FactoryTypeExtra methods
Schema::text($name)text->multiline(), ->mono()
Schema::number($name)number->min($v), ->max($v), ->step($v)
Schema::select($name)select->options([value => label]), ->multiple()
Schema::checkbox($name)checkbox->options([...])
Schema::json($name)jsonfree-form JSON value
Schema::date($name) / Schema::datetime($name)date / datetime
Schema::email($name) / Schema::password($name)email / password
Schema::builder($name)buildervalue is itself a designed schema (used by the Manual trigger's payload)
Schema::group($name)group->fields([...]), ->resolver(Resolver::class)
Schema::dynamic($name, Resolver::class)groupshorthand for a group with a resolver
Schema::whereBuilder($name) / Schema::filterBuilder($name)where/filter builder->operators([...])

Common modifiers

Available on every field:

Schema::text('token')
->label('API token')
->required() // or ->required(false)
->help('Where to find it…')
->placeholder('sk_live_…')
->default('…') // seeds the builder's prompt
->sensitive() // encrypted at rest
->hidden() // not shown in the form
->disabled() // shown, not editable
->rules('required|url') // Laravel validation rules
->when('mode', ['advanced']); // conditional visibility

->when($field, $value)

The field is only shown/prompted when another field in the same form equals $value (an array means "any of these"). Used throughout the built-in nodes — e.g. the HTTP node's body only appears for write methods.

->handle($callback) — deferred definition

For values that must be computed at resolve time (a list of the app's filesystem disks, the workflows available to call), pass a closure. It receives the field and can mutate it fluently or return an array to merge:

Schema::select('disk')
->handle(fn ($field) => $field->options(
collect(array_keys(config('filesystems.disks')))
->mapWithKeys(fn ($d) => [$d => $d])->all()
));

Dynamic field groups

A group field with a resolver produces its child fields from answers already given in the same session — for example, the Model nodes' attributes group turns the chosen model's fillable attributes into fields.

A resolver implements the marker interface DynamicSchemaResolver and defines a resolve() method in one of two shapes:

use Qanna\WorkflowEngine\Engine\Contracts\DynamicSchemaResolver;
use Qanna\WorkflowEngine\Engine\Schema\Schema;

class WebhookHeadersResolver implements DynamicSchemaResolver
{
// Shape A: the answers collected so far in this layer
public function resolve(array $answers): array
{
$preset = $answers['preset'] ?? null;

return $preset === 'json'
? [Schema::text('content_type')->default('application/json')]
: [];
}
}
use Qanna\WorkflowEngine\Engine\Context\DynamicSchemaContext;

class ConnectionAwareResolver implements DynamicSchemaResolver
{
// Shape B: cross-layer access (config / connection / advanced answers)
public function resolve(DynamicSchemaContext $context): array
{
$provider = $context->connection('provider');
// ->config($key), ->connection($key), ->advanced($key), ->get('connection.provider')
return [/* fields depending on $provider */];
}
}

The engine picks whichever signature you declared. Attach a resolver with Schema::group('headers')->resolver(WebhookHeadersResolver::class) or the Schema::dynamic('headers', WebhookHeadersResolver::class) shorthand.

Custom field prompters (CLI)

The interactive builder knows how to prompt for the built-in field types. To teach it a type it doesn't handle natively — say a bespoke key-value field your package's UI renders — register a FieldPrompter from a service provider:

use Qanna\WorkflowEngine\Console\Support\FieldPrompterRegistry;
use Qanna\WorkflowEngine\Console\Support\Contracts\FieldPrompter;

class KeyValuePrompter implements FieldPrompter
{
public function prompt(array $field, mixed $existing): mixed
{
// use laravel/prompts here; return the collected value
}
}

$this->app->make(FieldPrompterRegistry::class)->register('key-value', new KeyValuePrompter());

SchemaFieldPrompter consults the registry for any field type it doesn't handle itself.