actualbudget/actual · error

Unable to duplicate a budget that is not local.

Error message

Unable to duplicate a budget that is not local.

What it means

The `duplicateBudget` thunk in the desktop-client budgetfiles slice throws this when called without a local budget `id`. Duplication works by asking the backend to copy an existing local budget file, so a truthy `id` is a precondition. `DuplicateBudgetPayload.id` is optional (`id?: string`), so a caller that only supplies a `cloudId` (or forgets the id) reaches this guard. The guard exists because cloud-only budgets have no local file to duplicate.

Source

Thrown at packages/desktop-client/src/budgetfiles/budgetfilesSlice.ts:203

   */
  cloudSync: boolean;
};

export const duplicateBudget = createAppAsyncThunk(
  `${sliceName}/duplicateBudget`,
  async (
    {
      id,
      oldName,
      newName,
      managePage,
      loadBudget = 'none',
      cloudSync,
    }: DuplicateBudgetPayload,
    { dispatch },
  ) => {
    if (!id) {
      throw new Error('Unable to duplicate a budget that is not local.');
    }

    try {
      dispatch(
        setAppState({
          loadingText: t('Duplicating: {{oldName}} to: {{newName}}', {
            oldName,
            newName,
          }),
        }),
      );

      await send('duplicate-budget', {
        id,
        newName,
        cloudSync,
        open: loadBudget,
      });

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Ensure the payload includes the local budget's `id` (the local file id from `loadAllFiles`/budget metadata), not just `cloudId`.
  2. Only offer the Duplicate action for budgets that are downloaded locally; hide or disable it for cloud-only entries.
  3. If the budget is remote-only, first download it (downloadBudget with its cloudFileId) and then duplicate using the returned local id.
  4. Check for stale file metadata — refresh the file list so the local `id` is populated before dispatching.

Example fix

// before
await dispatch(duplicateBudget({ cloudId: file.cloudFileId, oldName: file.name, newName, cloudSync: true }));
// after
if (!file.id) {
  alert('Download the budget before duplicating it.');
  return;
}
await dispatch(duplicateBudget({ id: file.id, oldName: file.name, newName, cloudSync: true }));
Defensive patterns

Strategy: validation

Validate before calling

const canDuplicate = (file?: { id?: string; cloudFileId?: string }) =>
  typeof file?.id === 'string' && file.id.length > 0;
if (!canDuplicate(file)) {
  alert('This budget is not downloaded locally and cannot be duplicated.');
  return;
}
await dispatch(duplicateBudget({ id: file.id, ... }));

Type guard

function hasLocalId(
  file: { id?: string | undefined },
): file is { id: string } {
  return typeof file.id === 'string' && file.id.length > 0;
}

Try / catch

try {
  await dispatch(duplicateBudget({ id, oldName, newName, cloudSync })).unwrap();
} catch (e) {
  if (e instanceof Error && e.message.includes('not local')) {
    alert('Download this budget first before duplicating it.');
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling `dispatch(duplicateBudget({...}))` where the payload's `id` field is undefined, null, or an empty string — e.g. dispatching from the budget manager for a file row that only has a `cloudId` (remote-only budget not yet downloaded), or a caller constructing the payload without `id`.

Common situations: User clicks 'Duplicate' on a budget in the manager page that exists only in the cloud (never downloaded locally); a UI regression drops the id from the payload; a custom integration/script reuses the thunk with only cloud metadata.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/51a8fa4fbbb1e848. Report an issue: GitHub.