Manual Decision Node Editor
Admins handle pending decisions from Manual Decision nodes here. Browse processes that have queued items, open one to review user submissions, and approve or decline individually or in bulk. Lives at /manual-editor.
Overview
When a workflow reaches a Manual Decision node, the user sees a waiting message and the workflow pauses. The item lands in the Admin Approvals queue. An admin then reviews and decides. Once decided, the workflow resumes on the matching branch.
Manual Decision node fires in a workflow
Admins and super-admins only
/admin/approvals
Process Selection
Entry screen. Each card shows a process that has at least one Manual Decision node. The badge counts pending items. Click any card to open the approval table.
Pending Approvals
20 total pending across 3 processes
Admissions
Application Review
12
Scholarships
Financial Aid Review
5
Interviews
Interview Decision
3
Mentoring
Mentor Approval
0
Card width
responsive, 2-col on sm+Color stripe
4px topprocess accent colorPending badge
36px circleprocess color, white textEmpty (zero pending)
muted cardgray400 stripe + opacity 0.7Title font
18px600 weightDecision label
14pxgray500On click
Navigate to approval table for that processApproval Table
Browse pending items for a single process. Core columns are checkbox, date, and user. Dynamic columns come from the workflow's configured context variables. Use the search field to filter by user name, email, or any other field—type to narrow the list.
Admissions: Application Review
Jan 15, 2024
John Smith
john@email.com
United States
1995
APP-2024-001Jan 16, 2024
Jane Doe
jane@email.com
Canada
1992
APP-2024-002Jan 17, 2024
Mike Johnson
mike@email.com
United Kingdom
1998
APP-2024-003Jan 18, 2024
Sarah Wilson
sarah@email.com
Australia
2001
APP-2024-004Jan 18, 2024
Carlos Rivera
carlos@email.com
Mexico
1996
APP-2024-005Core columns
checkbox, created, user (name + email)Dynamic columns
from workflow contextVariables settingSearch
TextField with icon, matches name, email, and all dynamic fieldsRow select
Checkboxprimary tint when checkedPer-row action
Decide buttonoutlined smallBulk actions
Approve / Declineappears when 1+ selectedEmpty cells
show as em dash—Horizontal scroll
min-width 720pxmobile-friendlySingle Decision
Click Decide on any row above to open the decision modal. Choose Approve or Decline, optionally add a comment, and submit. The row leaves the queue and the workflow resumes on the matching branch.
Trigger
Decide button per rowContainer
DialogMUI modalDecision input
RadioGroupApprove / DeclineComment field
TextField multilineoptionalSubmit
primary containedCancel
outlinedOn submit
Row removed, toast confirmation, workflow resumesError
stale data → refresh row, network → retryBulk Decision
Select multiple rows using the checkbox column to reveal the bulk actions. Choose Approve or Decline for the entire selection. Add one comment that applies to all items. Confirm before submitting.
Trigger
1+ rows selectedToolbar
Selected count chip + Approve + DeclineApprove button
contained successgreenDecline button
outlined errorredModal
Dialog with item count + comment fieldComment field
applies to all selectedsingle fieldConfirmation
Review count before submitProgress
loading state during submissionEmpty & Loading States
Cover every condition the page can land in.
All caught up
No processes have pending approvalsQueue cleared
All items in this process have been decidedThis item was already decided
Another admin acted on this item before you. Refresh to see the latest queue.
Information Architecture
Two screens, one decision flow. Always available back-navigation to the parent screen.
Process Selection
/admin/approvalsApproval Table
/admin/approvals/:processIdDecision (modal)
overlayCode
<Paper
onClick={() => router.push(`/admin/approvals/${process.id}`)}
sx={{
cursor: 'pointer',
border: '1px solid',
borderColor: 'divider',
borderRadius: '10px',
overflow: 'hidden',
'&:hover': { borderColor: 'primary.main', boxShadow: 1 },
}}
>
<Box sx={{ height: 4, backgroundColor: process.color }} />
<Box sx={{ p: 2.5, display: 'flex', justifyContent: 'space-between' }}>
<Box>
<Typography variant="h4">{process.name}</Typography>
<Typography variant="body2" color="text.secondary">
{process.decision}
</Typography>
</Box>
<Avatar sx={{ bgcolor: process.color }}>{process.pending}</Avatar>
</Box>
</Paper>// Columns merge core (always present) with workflow.contextVariables
const coreColumns = [
{ field: 'select', width: 44, renderCell: SelectCheckbox },
{ field: 'created', headerName: 'Created', width: 120 },
{ field: 'user', headerName: 'User', flex: 1, renderCell: UserCell },
];
const dynamicColumns = workflow.contextVariables.map((v) => ({
field: v,
headerName: humanize(v),
flex: 1,
valueGetter: (params) => params.row.context[v] ?? '—',
}));
const actionColumn = {
field: 'action',
headerName: 'Action',
width: 100,
renderCell: (params) => (
<Button size="small" variant="outlined" onClick={() => decide(params.row)}>
Decide
</Button>
),
};
<DataGrid
rows={pendingItems}
columns={[...coreColumns, ...dynamicColumns, actionColumn]}
checkboxSelection
onSelectionModelChange={setSelected}
/><Dialog open={!!decisionRow} onClose={() => setDecisionRow(null)} maxWidth="sm" fullWidth>
<DialogTitle>Review application</DialogTitle>
<DialogContent dividers>
<RadioGroup
value={decision}
onChange={(e) => setDecision(e.target.value)}
>
<FormControlLabel value="approve" control={<Radio />} label="Approve" />
<FormControlLabel value="decline" control={<Radio />} label="Decline" />
</RadioGroup>
<TextField
label="Comment (optional)"
value={comment}
onChange={(e) => setComment(e.target.value)}
multiline
minRows={3}
fullWidth
sx={{ mt: 2 }}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setDecisionRow(null)} variant="outlined">Cancel</Button>
<Button onClick={handleSubmit} variant="contained" disabled={!decision}>
Submit
</Button>
</DialogActions>
</Dialog>