Portal Community

Handler Signature

import type { FormActionContext } from '@atlas-forms/types-js';

// The required function signature
type FormActionHandler = (context: FormActionContext) => Promise<void>;

// Minimum valid handler
const myHandler: FormActionHandler = async (ctx) => {
  // ... do work ...
  ctx.complete();
};

Full Handler with Error Handling

import type { FormActionContext } from '@atlas-forms/types-js';
import { approvalService } from '../services/approvalService';

export const approveWorkflowHandler: FormActionHandler = async (ctx) => {
  // 1. Validate first if needed
  const isValid = await ctx.validate();
  if (!isValid) {
    ctx.fail('Please fix form errors before approving.');
    return;
  }

  // 2. Extract values
  const { formValues, tenantId, config } = ctx;
  const workflowInstanceId = formValues['workflow-instance-id'];
  const approverNotes = formValues['approver-notes'];
  const transitionTo = config?.transitionTo ?? 'approved';

  // 3. Guard — ensure we have required data
  if (!workflowInstanceId) {
    ctx.fail('No workflow ID found. Cannot approve.');
    return;
  }

  try {
    // 4. Call your service
    await approvalService.approve({
      workflowInstanceId,
      tenantId,
      notes: approverNotes,
      status: transitionTo
    });

    // 5. Optionally update a field in the form to reflect new status
    ctx.formEngine.setFieldValue('approval-status', transitionTo);

    // 6. Signal success and navigate away
    ctx.complete('Workflow approved successfully.');
    ctx.navigate('/workdesk/inbox');

  } catch (error) {
    // 7. Always call fail() so the button resets
    const message = error instanceof Error ? error.message : 'Unknown error';
    ctx.fail(`Approval failed: ${message}`);
  }
};

Async Patterns

Sequential Async Calls

const handler: FormActionHandler = async (ctx) => {
  const step1 = await serviceA.doThing(ctx.formValues);
  const step2 = await serviceB.doOther(step1.result);
  ctx.complete(`Done: ${step2.summary}`);
};

Parallel Async Calls

const handler: FormActionHandler = async (ctx) => {
  const [result1, result2] = await Promise.all([
    serviceA.save(ctx.formValues),
    serviceB.notify(ctx.tenantId, ctx.formId)
  ]);
  ctx.complete();
};

Conditional Async

const handler: FormActionHandler = async (ctx) => {
  const { config, formValues } = ctx;

  if (config?.requireApproval) {
    await approvalQueue.enqueue(formValues);
    ctx.complete('Sent for approval');
  } else {
    await directSubmit(formValues);
    ctx.complete('Submitted directly');
  }
};

Common Pitfalls

PitfallSymptomFix
Not calling complete() or fail()Button stuck in loading state foreverAlways call one in every code path
Calling complete() after navigate()May try to update unmounted componentCall navigate() after complete()
Throwing instead of calling fail()Generic error shown, no messageCatch errors and call fail(message)
Using formValues after engine changesStale dataUse ctx.formEngine.getValues() for fresh snapshot