---
url: "https://vueda.dev/v3/guides/transition-ux-and-redirects.md"
title: "Design Transition UX and Redirects"
---

# Design Transition UX and Redirects

This guide shows how an action form runs a workflow [Transition](/v3/reference/glossary.md#transition), and how to choose the view that an action form opens after a submit or cancel. The redirect rules apply to every view built on [ModelActionForm](/v3/reference/api/vue/ModelActionForm.md), including destroy, extra actions, and transitions.

## How a Transition Route Reaches Its Form

A transition route carries the transition's `code` as its action segment. [Route Admission](/v3/reference/glossary.md#route-admission) accepts it when the code is one of the user's [Permitted Transitions](/v3/reference/glossary.md#permitted-transitions). [ViewActionRouter](/v3/reference/api/vue/ViewActionRouter.md) then renders a project view named for the code. When no such view exists, it renders [ViewExecuteTransition](/v3/reference/api/vue/ViewExecuteTransition.md). [Routing and View Resolution Model](/v3/core-concepts/routing-and-view-resolution-model.md#view-component-resolution-order) gives the full resolution order.

`ViewExecuteTransition` wraps `ModelActionForm` and replaces its [`run-action`](/v3/reference/api/vue/ActionForm.md#prop-runAction) with a call to [executeTransition](/v3/reference/api/js/stores/storeWorkflow/methods/executeTransition.md). A transition therefore gets the same pre-flight, submit, cancel, and redirect behavior as any other action form. [Action & Workflow Views](/v3/reference/components/action-workflow.md#viewexecutetransition) describes what its confirmation shows.

The view uses the transition's `name` for the page title and the confirmation prompt. Routing and execution use only the `code`, which the view passes to `executeTransition` unchanged. When you change a transition's `code`, also rename any project view named for it (`ViewAction<Code>.vue` or `ViewAction<App><Model><Code>.vue`) and update any route links that your application builds with it.

## What an Action Form Does on Pre-Flight, Submit, and Cancel

[ActionForm](/v3/reference/api/vue/ActionForm.md) runs all three paths. `ModelActionForm` connects it to the model through [useModelAction](/v3/reference/api/js/use/useModelAction/functions/useModelAction.md).

- **Pre-flight.** Once the form has at least one primary key, it runs the action as a [Dry Run](/v3/reference/glossary.md#dry-run), with the `Dry-Run: true` header. Validation errors from the pre-flight appear on the form before the user confirms. The pre-flight shows no toast, does not redirect, and does not open the confirmation dialog for a `409` warnings response. It runs again when the app, model, action, or primary keys change. Set [`enableDryRun`](/v3/reference/api/vue/ModelActionForm.md#prop-enableDryRun) to `false` to turn it off.
- **Submit.** The form runs the action. On success it shows a toast and calls `redirectTo("success")`. With [`onSubmissionSuccessHandler`](/v3/reference/api/vue/ActionForm.md#prop-onSubmissionSuccessHandler) set, it calls that handler instead of both. On a `400`, the errors appear on the form. A `409` opens [FormConfirmDialog](/v3/reference/api/vue/FormConfirmDialog.md), and confirming retries with the warnings acknowledged ([Warning Confirmation](/v3/reference/glossary.md#warning-confirmation)). Other failures show an error toast.
- **Cancel.** The form calls `redirectTo("cancel")` without running the action.

After a success redirect navigates away, the form disables its buttons and ignores submits until the view unmounts. The action cannot run twice while the next view loads. A [`redirectTo`](/v3/reference/api/vue/ActionForm.md#prop-redirectTo) that finishes without navigating must resolve `false`, and the form then accepts input again. In a [slot override](/v3/reference/api/vue/ActionForm/slots.md), bind `disabled` from the `confirm-button` and `cancel-button` slots, or `confirmDisabled` and `cancelDisabled` from `action-bar`. These flags control the buttons only. The server still decides whether the action runs.

`ModelActionForm` passes `ActionForm` a [`dryRunTarget`](/v3/reference/api/vue/ActionForm.md#prop-dryRunTarget) that changes with the target. If you build a view on `ActionForm` directly and its target can change after mount, pass `dryRunTarget` along with [`readyToDryRun`](/v3/reference/api/vue/ActionForm.md#prop-readyToDryRun). Without it, the pre-flight runs once for the life of the form.

For a transition, `ViewExecuteTransition` forwards the dry-run flag and the acknowledged warnings digest to `executeTransition`. The store rejects a `400` with [FormValidationError](/v3/reference/api/js/utils/errors/members/FormValidationError.md) and a `409` with [ConfirmationRequiredError](/v3/reference/api/js/utils/errors/members/ConfirmationRequiredError.md), so both reach the form the same way as for other actions. A dry run keeps the store's cached state history. A completed transition drops the cached history of each object that it ran on, so the next history request goes to the server.

## Redirect Precedence and Route Targets

The form's [`redirectTo`](/v3/reference/api/js/use/useModelAction/types/ModelActionContext.md#redirectTo) picks the destination in this order, for both success and cancel:

1. **`returnPath` query value.** When the current route has a `returnPath` query value, the form navigates to it. Add `returnPath` to a link into an action view to bring the user back to where they started.
2. **`actionRedirects` entry.** Otherwise the form reads [`actionRedirects`](/v3/reference/api/js/stores/storeModelConfig/types/ModelConfig.md#actionRedirects) from the model's [Model Config](/v3/reference/glossary.md#model-config): the entry for the route action, else `default`. A successful `destroy` with no `destroy` entry goes to the list view, because the deleted record has no detail view. Cancelling a destroy uses `default`.
3. **Function entries.** An entry can be a function. The form calls it with `{ bulk, result }` and uses its return value. `result` is `"success"` or `"cancel"`, and `bulk` is `true` when the action targeted more than one record.
4. **Route.** When the action targeted more than one record, or the value is `"list"`, the form opens the model's list view. Otherwise it opens the detail route for the first targeted record, with the value as the route action.

The built `default` is `"update"` when [Model Info](/v3/reference/glossary.md#model-info) lists `update`, else `"read"` when it lists `retrieve`, else `"list"` when it lists `list`. A model whose model info lists none of the three has no built default, so set `default` yourself.

To change the redirects for a model:

1. Call [`setConfig`](/v3/reference/api/js/stores/storeModelConfig/methods/setConfig.md) with `actionRedirects` in the generic config, the second argument. Action forms read the generic layer only.
2. Key each entry by the route action: an extra action's name, a transition code, or `destroy`. Add `default` for the actions that you do not list. Your entries merge with the built ones key by key.
3. Use route segments as values, such as `"list"`, `"read"`, `"update"`, or another action's name. The detail view's segment is `read`. The [Canonical Action Name](/v3/reference/glossary.md#canonical-action-name) `retrieve` opens the generic action view.

```js
import { storeModelConfig } from "@vueda/stores/storeModelConfig.js";

const modelConfigStore = storeModelConfig();

modelConfigStore.setConfig(
    { app: "myapp", model: "order" },
    {
        actionRedirects: {
            approve: "list",
            reopen: ({ result }) => (result === "success" ? "update" : "read"),
            default: "read",
        },
    },
);
```

### Navigate by the New State in a Project View

A project view that calls `executeTransition` itself can route by the object's new state. Pass the [`router`](/v3/reference/api/js/stores/storeWorkflow/methods/executeTransition.md#param-router) and a [`stateToRoute`](/v3/reference/api/js/stores/storeWorkflow/methods/executeTransition.md#param-stateToRoute) map from state code to route. After a single-object run that is not a dry run, the store navigates to the route for the new state's code, when the map has one. `ViewExecuteTransition` passes no map, so it uses `actionRedirects`. In a view built on `ActionForm`, the success redirect also navigates, so use one or the other.

## Send the Transition Request

`executeTransition` sends `PATCH` to the workflow execute-transition endpoint:

- For one object, it calls [`execute-transition/{object_id}/`](/v3/reference/api/rest/vueda.workflow/vueda.workflow_workflows_execute_object_transition_partial_update.md) with `{ "transition_code": "approve" }`.
- For an array of primary keys, it calls [`execute-transition/`](/v3/reference/api/rest/vueda.workflow/vueda.workflow_workflows_execute_transition_partial_update.md) with `{ "transition_code": "approve", "object_ids": [...] }`. `object_ids` must be a list.

Generic bulk model actions send their keys as `pks`. Client code that calls the endpoint without the store must send `object_ids`.

[WorkflowViewSet.execute\_transition](/v3/reference/api/py/vueda.workflow.viewsets/WorkflowViewSet.md#execute_transition) answers a missing `transition_code` with `400`. For a single object, it then runs these checks in order:

1. It checks that the user can read the object.
2. It checks the workflow and transition permissions, and that the transition leaves the object's current state. An unknown `transition_code` also fails here, with `400`.
3. It gates the write on the model's [`get_transition_warnings`](/v3/reference/api/py/vueda.workflow.models/WorkflowModelMethods.md#get_transition_warnings), returning `409` until the user acknowledges the warnings. [Require Confirmation Before a Write](/v3/guides/require-write-confirmation.md#warn-on-a-workflow-transition) shows how to add warnings.
4. Outside a dry run, it locks the row with [Django: QuerySet.select\_for\_update](https://docs.djangoproject.com/en/6.1/ref/models/querysets/#django.db.models.query.QuerySet.select_for_update) and `skip_locked=True`. When another request holds the lock, it returns `400` with "This object cannot be updated right now. Please try again."
5. It repeats the object and transition checks on the locked row, writes the new state, and calls [`on_transition`](/v3/reference/api/py/vueda.workflow.models/WorkflowModelMethods.md#on_transition).

The response holds `new_state` and `new_transitions`, the transitions the user can take from the new state. A bulk request checks every object and gates warnings once for the whole batch before it writes any. Its warnings and its response are keyed by object id. [Permissions](/v3/reference/permissions.md#status-codes) lists the status code for each refusal.

A dry run skips the row lock. The server writes the transition, and the request's rollback discards it. `on_transition` receives `dry_run=True`, so skip external side effects there during a dry run.

## Verification Checklist

- Opening a permitted transition route renders `ViewExecuteTransition` or your project view.
- Opening the view shows pre-flight validation errors before you confirm, and nothing changes on the server.
- Submitting sends the transition `code` as `transition_code` and redirects according to `actionRedirects`.
- A `returnPath` query value overrides the configured redirect.
- Cancelling redirects without running the transition.
- A multi-record transition sends `object_ids` and lands on the list view.
- A transition with unacknowledged warnings opens the confirmation dialog. Confirming completes the transition, and cancelling leaves it unapplied.
- A transition attempted while another request holds the row lock shows the lock message.
