Skip to main content
Wildo.ai Coming soon

Files and storage

Keep attachments with the part they describe

File handling follows supported nested objects and repeated items, so attachments can stay inside the record structure that gives them meaning.

Multiple attachments and a nested team avatar sit in the parts of the project they describe.

Keep attachments with the part they describe

Attachments sometimes belong to a line item or a section inside a record. Wildo can find file fields inside objects and arrays, preserving their field identity as the record changes.

The same discovery is used for upload routes, constraints and attachment reconciliation. A repeated field does not need a separate file feature for every array position.

Example — A photograph on each inspection item

An inspection contains several checks, each with its own photo. Reordering the checks should not turn the photo field into a different upload endpoint. The schema keeps one canonical field path while record reconciliation follows the actual occurrences.

For engineers

Distinguish a field path from an array position

The file-field walker uses object keys to extend the canonical path and passes through arrays without adding an index. These are two branches of collectFileFieldSchemaMetadata; the branches between them are omitted.

if (isZodArray(unwrappedSchema)) {
  const elementSchema = getZodDefProperty(unwrappedSchema, 'element') as z.ZodTypeAny | undefined;
  return collectFileFieldSchemaMetadata(elementSchema, pathPrefix, activeSchemas);
}
if (isZodObject(unwrappedSchema)) {
  const shape = getZodObjectShape(unwrappedSchema);
  return Object.entries(shape).flatMap(([fieldName, fieldSchema]) => {
    const nextPath = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
    return collectFileFieldSchemaMetadata(fieldSchema as z.ZodTypeAny, nextPath, activeSchemas);
  });
}

Use a supported schema shape

Objects, arrays and discriminated-union object variants can carry file fields. The same canonical path across variants must have compatible file metadata, since one upload route cannot enforce contradictory constraints. Optional, nullable and default wrappers are unwrapped during discovery.

Plain unions containing file fields and dynamic containers such as records, maps, sets and tuples are rejected rather than silently losing their attachments. Use a discriminant for variants and an array of named objects for repeated content.

Reconcile the effective record, not just the submitted fragment

The operation handler compares file occurrences in the previous and effective next resource state. Object patches preserve omitted siblings, but a supplied array replaces the previous array; omitted photos inside that replacement are removed. Repeated items can use their stable identifier to retain ownership through reordering, while explicit removal applies the field’s lifecycle policy.

A file can be attached to one record and field binding at a time. Do not treat a repeated field as permission to reuse a file ID under unrelated records; the same scope, status and ownership checks still apply.

Declare one photo field for every inspection item

This illustrative schema gives each repeated item a stable id and an optional photo. Load the Zod decorators once in shared initialization, as in the framework’s schema tests.

import { z } from 'zod';
import { ensureZodDecoratorsLoaded, z_file } from '@wildo-ai/zod-decorators';
ensureZodDecoratorsLoaded(z);

export const InspectionSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    label: z.string(),
    photo: z_file({
      multiple: false,
      allowedMimeTypes: ['image/jpeg', 'image/png'],
      maxSize: 5 * 1024 * 1024,
    }).optional(),
  })),
});

Discovery produces the canonical field name items.photo. Both items[0].photo and items[1].photo are occurrences of that field; array indices do not become separate upload contracts. After uploading an image and waiting for it to become attachable, a parent request can contain:

{
  "items": [
    {
      "id": "entrance",
      "label": "Entrance condition",
      "photo": {
        "fileId": "507f1f77bcf86cd799439011",
        "updatedAt": "2026-09-12T09:00:00.000Z"
      }
    },
    { "id": "roof", "label": "Roof condition" }
  ]
}

The file ID stands for an actual upload in the caller’s scope; copying the illustrative ID cannot create that file. The backend binds it to the parent record and items.photo after checking its status and ownership.

Next writeAttachment outcome
Reorder the complete items while preserving their IDs and photo referencesThe same file remains attached to the same record and canonical field
Omit the entire items field from a parent patchThe existing array and its photos remain in the effective state
Supply an items array with the same item ID but omit its photoThe array replaces the old value; the omitted photo reference is removed, even though the item ID is unchanged
Explicitly replace the array and remove the item carrying the photoThe removed reference is reconciled under the field’s configured lifecycle policy
Move that file ID into another recordThe existing ownership binding prevents treating the ID as a freely reusable upload

Use stable, non-empty string id or _id values on repeated owners. The reconciliation helper recognizes these identifiers when matching occurrences across updates. Stable identifiers match occurrences; they do not merge missing photo values into a replacement array. Send the complete intended array, including every reference you want to keep. For nested object patches, omitted sibling properties are preserved.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.