actualbudget/actual · error

Invalid id, must be string:

Error message

Invalid id, must be string: 

What it means

Values cast to the `id` type must be a string (or explicitly null). Ids in Actual are opaque strings; passing a number, object, or other non-string type indicates a bug and is rejected with 'Invalid id, must be string'.

Source

Thrown at packages/loot-core/src/server/aql/schema-helpers.ts:42

      if (value instanceof Date) {
        return toDateRepr(dayFromDate(value));
      } else if (
        value.match(/^\d{4}-\d{2}-\d{2}$/) == null ||
        value < '1995-01-01'
      ) {
        throw new Error('Invalid date: ' + value);
      }

      return toDateRepr(value);
    case 'date-month':
      return toDateRepr(value.slice(0, 7));
    case 'date-year':
      return toDateRepr(value.slice(0, 4));
    case 'boolean':
      return value ? 1 : 0;
    case 'id':
      if (typeof value !== 'string' && value !== null) {
        throw new Error('Invalid id, must be string: ' + value);
      }
      return value;
    case 'integer':
      if (typeof value === 'number' && Number.isInteger(value)) {
        return value;
      } else {
        throw new Error("Can't convert to integer: " + JSON.stringify(value));
      }
    case 'json':
      return JSON.stringify(value);
    default:
  }
  return value;
}

export function convertOutputType(value, type) {
  if (value === null) {
    if (type === 'boolean') {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Convert the value to a string before querying: `String(id)` — but only if it is a legitimate id.
  2. Generate proper Actual ids with the id utilities (e.g. `.randomUUID()` based ids) instead of external numeric keys.
  3. If the id may legitimately be absent, pass null explicitly rather than 0 or a number.
  4. Trace where the non-string id originates (importer, API response) and fix the mapping layer.

Example fix

// before
q('transactions').filter({ account: row.accountId }); // number 42
// after
q('transactions').filter({ account: row.accountId != null ? String(row.accountId) : null });
Defensive patterns

Strategy: type-guard

Validate before calling

const isValidId = (v) => typeof v === 'string' && v.length > 0;
if (!isValidId(id)) throw new Error('caller must supply a string id');

Type guard

const isStringId = (v) => typeof v === 'string';

Try / catch

try {
  return await runQuery(q.filter({ account: id }));
} catch (e) {
  if (e.message.startsWith('Invalid id, must be string:')) {
    logger.error('Non-string id passed to query', { id });
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a numeric id (e.g. from a CSV import or external system), an auto-increment integer, or an object/undefined that is not null into an `id`-typed query field such as account, payee, or transaction id.

Common situations: Importing data from other finance tools that use integer keys, comparing ids parsed from JSON where numbers leaked in, or using a library/entity record where the id field was never converted to Actual's string id format.

Related errors


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