diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts
index 5fdfdbc2ca4eea7ad4c06db994742426cbc10ad1..4fdbf9adbd4f7c697c51677371c10583446576b6 100644
--- a/src/mol-plugin/state/actions/basic.ts
+++ b/src/mol-plugin/state/actions/basic.ts
@@ -18,10 +18,10 @@ export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root
     },
     params: {
         default: () => ({ id: '1grm' }),
-        controls: () => ({
+        definition: () => ({
             id: PD.Text('PDB id', '', '1grm'),
         }),
-        validate: p => !p.id || !p.id.trim() ? ['Enter id.'] : void 0
+        validate: p => !p.id || !p.id.trim() ? [['Enter id.', 'id']] : void 0
     },
     apply({ params, state }) {
         const url = `http://www.ebi.ac.uk/pdbe/static/entry/${params.id.toLowerCase()}_updated.cif`;
diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts
index a3379657811969c19913feae848307dc058f74b5..2f3e631cf59919520481560d1f8dfa9bc3fc5eff 100644
--- a/src/mol-plugin/state/transforms/data.ts
+++ b/src/mol-plugin/state/transforms/data.ts
@@ -26,12 +26,12 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B
         default: () => ({
             url: 'https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif'
         }),
-        controls: () => ({
+        definition: () => ({
             url: PD.Text('URL', 'Resource URL. Must be the same domain or support CORS.', ''),
             label: PD.Text('Label', '', ''),
             isBinary: PD.Boolean('Binary', 'If true, download data as binary (string otherwise)', false)
         }),
-        validate: p => !p.url || !p.url.trim() ? ['Enter url.'] : void 0
+        validate: p => !p.url || !p.url.trim() ? [['Enter url.', 'url']] : void 0
     },
     apply({ params: p }, globalCtx: PluginContext) {
         return Task.create('Download', async ctx => {
diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts
index 93b5907132aa1f2a9675901878dca5a59ddf7479..2ad0eed9f445eeab6b1fd8bc8c0cab8c1825d7a8 100644
--- a/src/mol-plugin/state/transforms/model.ts
+++ b/src/mol-plugin/state/transforms/model.ts
@@ -25,7 +25,7 @@ const ParseTrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Mol
     to: [SO.Molecule.Trajectory],
     params: {
         default: a => ({ blockHeader: a.data.blocks[0].header }),
-        controls(a) {
+        definition(a) {
             const { blocks } = a.data;
             if (blocks.length === 0) return {};
             return {
@@ -58,7 +58,7 @@ const CreateModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajec
     to: [SO.Molecule.Model],
     params: {
         default: () => ({ modelIndex: 0 }),
-        controls: a => ({ modelIndex: PD.Range('Model Index', 'Model Index', 0, 0, Math.max(0, a.data.length - 1), 1) })
+        definition: a => ({ modelIndex: PD.Range('Model Index', 'Model Index', 0, 0, Math.max(0, a.data.length - 1), 1) })
     },
     isApplicable: a => a.data.length > 0,
     apply({ a, params }) {
@@ -103,7 +103,7 @@ const CreateStructureAssembly = PluginStateTransform.Create<SO.Molecule.Model, S
     to: [SO.Molecule.Structure],
     params: {
         default: () => ({ id: void 0 }),
-        controls(a) {
+        definition(a) {
             const model = a.data;
             const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]);
             return { id: PD.Select('Asm Id', 'Assembly Id', ids.length ? ids[0][0] : '', ids) };
diff --git a/src/mol-plugin/ui/state/parameters.tsx b/src/mol-plugin/ui/state/parameters.tsx
index 804770f15682002942091539a8ecc29ff60d8412..81f3e591508199bab726d277561c581859f9092d 100644
--- a/src/mol-plugin/ui/state/parameters.tsx
+++ b/src/mol-plugin/ui/state/parameters.tsx
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateObject, Transformer, State, Transform, StateObjectCell } from 'mol-state';
+import { StateObject, State, Transform, StateObjectCell } from 'mol-state';
 import { shallowEqual } from 'mol-util/object';
 import * as React from 'react';
 import { PurePluginComponent } from '../base';
@@ -17,7 +17,7 @@ export { StateTransformParameters };
 
 class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> {
     getDefinition() {
-        const controls = this.props.info.definition.controls;
+        const controls = this.props.info.definition.definition;
         if (!controls) return { };
         return controls!(this.props.info.source, this.plugin)
     }
@@ -25,7 +25,9 @@ class StateTransformParameters extends PurePluginComponent<StateTransformParamet
     validate(params: any) {
         const validate = this.props.info.definition.validate;
         if (!validate) return void 0;
-        return validate(params, this.props.info.source, this.plugin)
+        const result = validate(params, this.props.info.source, this.plugin);
+        if (!result || result.length === 0) return void 0;
+        return result.map(r => r[0]);
     }
 
     areInitial(params: any) {
@@ -48,7 +50,7 @@ class StateTransformParameters extends PurePluginComponent<StateTransformParamet
 namespace StateTransformParameters {
     export interface Props {
         info: {
-            definition: Transformer.ParamsDefinition,
+            definition: PD.Provider,
             params: PD.Params,
             initialValues: any,
             source: StateObject,
@@ -68,7 +70,7 @@ namespace StateTransformParameters {
         const source = state.cells.get(nodeRef)!.obj!;
         const definition = action.definition.params || { };
         const initialValues = definition.default ? definition.default(source, plugin) : {};
-        const params = definition.controls ? definition.controls(source, plugin) : {};
+        const params = definition.definition ? definition.definition(source, plugin) : {};
         return {
             source,
             definition: action.definition.params || { },
@@ -82,7 +84,7 @@ namespace StateTransformParameters {
         const cell = state.cells.get(transform.ref)!;
         const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0;
         const definition = transform.transformer.definition.params || { };
-        const params = definition.controls ? definition.controls((source && source.obj) as any, plugin) : {};
+        const params = definition.definition ? definition.definition((source && source.obj) as any, plugin) : {};
         return {
             source: (source && source.obj) as any,
             definition,
diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts
index ad57879c7781e64c6d8993451a01cc93c59e6565..4898eb6be775af4fb07fa6e5ab52ffad16212f7d 100644
--- a/src/mol-state/action.ts
+++ b/src/mol-state/action.ts
@@ -46,7 +46,7 @@ namespace StateAction {
          */
         apply(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
 
-        readonly params?: Transformer<A, any, P>['definition']['params'],
+        readonly params?: PD.Provider<A, P, unknown>
 
         /** Test if the transform can be applied to a given node */
         isApplicable?(a: A, globalCtx: unknown): boolean
diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts
index 6588a55167bbc472d69cfb56a3cd1335ba8ea8bb..1bf6aaceef3ed8a28140113c1223e0bf7d56e4fe 100644
--- a/src/mol-state/transformer.ts
+++ b/src/mol-state/transformer.ts
@@ -47,17 +47,6 @@ export namespace Transformer {
 
     export enum UpdateResult { Unchanged, Updated, Recreate }
 
-    export interface ParamsDefinition<A extends StateObject = StateObject, P = unknown> {
-        /** Check the parameters and return a list of errors if the are not valid. */
-        default?(a: A, globalCtx: unknown): P,
-        /** Specify default control descriptors for the parameters */
-        controls?(a: A, globalCtx: unknown): ControlsFor<P>,
-        /** Check the parameters and return a list of errors if the are not valid. */
-        validate?(params: P, a: A, globalCtx: unknown): string[] | undefined,
-        /** Optional custom parameter equality. Use deep structural equal by default. */
-        areEqual?(oldParams: P, newParams: P): boolean
-    }
-
     export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
         readonly name: string,
         readonly from: StateObject.Ctor[],
@@ -77,7 +66,7 @@ export namespace Transformer {
          */
         update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult,
 
-        readonly params?: ParamsDefinition<A, P>,
+        readonly params?: PD.Provider<A, P, unknown>,
 
         /** Test if the transform can be applied to a given node */
         isApplicable?(a: A, globalCtx: unknown): boolean,
diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts
index 0ebf49dd83c29c74133912ff304a2ec92ec1128f..7d3d7cdb5ca7ea0fff33824dd2ad93bf81aa465b 100644
--- a/src/mol-util/param-definition.ts
+++ b/src/mol-util/param-definition.ts
@@ -90,4 +90,21 @@ export namespace ParamDefinition {
         Object.keys(params).forEach(k => d[k] = params[k].defaultValue)
         return d as { [k in keyof T]: T[k]['defaultValue'] }
     }
+
+    /**
+     * List of [error text, pathToValue]
+     * i.e. ['Missing Nested Id', ['group1', 'id']]
+     */
+    export type ParamErrors = [string, string | string[]][]
+
+    export interface Provider<A = any, P = any, Ctx = any> {
+        /** Check the parameters and return a list of errors if the are not valid. */
+        default?(a: A, globalCtx: Ctx): P,
+        /** Specify default control descriptors for the parameters */
+        definition?(a: A, globalCtx: Ctx): { [K in keyof P]?: Any },
+        /** Check the parameters and return a list of errors if the are not valid. */
+        validate?(params: P, a: A, globalCtx: unknown): ParamErrors | undefined,
+        /** Optional custom parameter equality. Use deep structural equal by default. */
+        areEqual?(oldParams: P, newParams: P): boolean
+    }
 }
\ No newline at end of file