Skip to main content
Wildo.ai Coming soon

Declaring a resource

Let a connection carry its own information

Model memberships, dependencies and other meaningful connections as records with their own fields.

Alex connects to a team through a membership carrying the Admin role.

Let a connection carry its own information

A membership can have a role and a start date. A connection between two tasks can mean that one blocks the other. The relationship itself contains information worth keeping.

Wildo can represent that connection as a resource. It has its own fields and operations while linking the records on either side, so the information about the connection has a clear home.

The same approach supports connections between records of the same kind. Explicit source and target fields make the direction of a task dependency understandable.

Example — Explain why two tasks are connected

“Prepare the launch” depends on “Approve the brief”. A connection record identifies both tasks and records the kind of relationship, instead of leaving the dependency hidden in a comment or an unexplained pair of IDs.

For engineers

Store facts about the connection

A junction resource represents the link, not either endpoint. Its fields can describe the role, status or type of the connection. The junction has its own resource configuration and access choices.

Wonder Todos models connections between tasks in todos-relationships.schemas.ts. This excerpt includes the fields and the check that the two ends differ:

export const TodosRelationships_BaseSchema = z.object({
  _id: z.string().min(1).isPrimaryKey().isSummaryField(),
  organizationId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromUpdate(),
  createdByUserId: z.string().min(1).isDBIndexed().isForeignKey().excludeFromUpdate(),
  todoId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromUpdate(),
  targetTodoId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromUpdate(),
  relationshipType: z.enum(TodosRelationships_Type).isSummaryField(),
  createdAt: z.date().isDBIndexed().isSummaryField().excludeFromCreate().excludeFromUpdate(),
  updatedAt: z.date().isDBIndexed().excludeFromCreate().excludeFromUpdate()
});

export const TodosRelationships_Schema = TodosRelationships_BaseSchema.refine(
  (data) => data.todoId !== data.targetTodoId,
  {
    message: "Source and target todos cannot be the same",
    path: ["targetTodoId"]
  }
);

todoId identifies the source task and targetTodoId the other task. relationshipType uses the application’s named vocabulary, which includes dependency and blocking relationships. The refinement attaches a same-task error to the target field so the invalid choice has a useful location.

Register both ends of a self-relationship

Two foreign keys alone do not tell the graph which task is the source, which is the target, or where the connection belongs. Wonder Todos declares three edges in tasks-manager.relationships.ts: an organization scope anchor, a source composition and a target reference. These declarations retain the actual options; source comments are omitted.

createResourcesRelationship(
  CoreResourceType.ORGANIZATIONS, TasksManager_ResourceType.TODOS_RELATIONSHIPS,
  ResourceRelationshipCardinality.ONE, ResourceRelationshipCardinality.MANY,
  {
    nature: RelationshipNature.COMPOSITION,
    isPrimaryScope: true,
    foreignKeyField: 'organizationId',
    contextPolicy: {}
  }
),

createResourcesRelationship(
  TasksManager_ResourceType.TODOS, TasksManager_ResourceType.TODOS_RELATIONSHIPS,
  ResourceRelationshipCardinality.ONE, ResourceRelationshipCardinality.MANY,
  {
    nature: RelationshipNature.COMPOSITION,
    foreignKeyField: 'todoId',
    allowCycle: true,
    accessScopeStrategy: ResourceRelationshipAccessScopeStrategy.OPTIONAL_CONTEXT,
    contextPolicy: {
      objectMode: ContextPolicy_ObjectMode.ID_ONLY,
      depth: 1,
    }
  }
),

createResourcesRelationship(
  TasksManager_ResourceType.TODOS, TasksManager_ResourceType.TODOS_RELATIONSHIPS,
  ResourceRelationshipCardinality.ONE, ResourceRelationshipCardinality.MANY,
  {
    nature: RelationshipNature.REFERENCE,
    foreignKeyField: 'targetTodoId',
    allowCycle: true,
    accessScopeStrategy: ResourceRelationshipAccessScopeStrategy.OPTIONAL_CONTEXT,
    contextPolicy: {
      objectMode: ContextPolicy_ObjectMode.ID_ONLY,
      depth: 1,
    }
  }
),
EdgeField on the connectionPurpose
Organization → connectionorganizationIdEstablishes the primary organization scope of the link record.
Source task → connectiontodoIdNames the task that owns this connection.
Target task → connectiontargetTodoIdReferences the other task without making it the owner.

The two task edges use different field identities even though they reach the same resource type. allowCycle: true admits this declared graph topology; it is not a promise that a dependency scheduler will detect or accept business cycles. Their ID_ONLY context and depth of one bound related-object expansion.

Bind the checked schema to the resource

The connection’s resource configuration uses the refined schema, not the unrefined base. Selected lines from todos-relationships.resources-config.ts show the binding:

createResourceConfiguration_Initialization({
  mainSchema: TodosRelationships_Schema,
  resourceIdentifier: TasksManager_ResourceType.TODOS_RELATIONSHIPS,
  resourceFieldIdentifier: TasksManager_ResourceFieldIdentifier[TasksManager_ResourceType.TODOS_RELATIONSHIPS],
  resourceRelationships: resourcesRelationships,
  // Other resource and operation settings are omitted.
});

This connects the source/target check to the derived write contract and gives the factory the graph it must interpret. Keep the field names, registered edges and the configuration’s schema in agreement. A source-equals-target error is one local rule; it does not prove that a longer chain of dependencies has no cycle.

Use a peer association for different resource types

Organization membership is a different setup: two distinct peer types connected through a named junction. The engine declares this association in resources-registry.shared.core.definitions.ts:

createResourcesRelationship(
    CoreResourceType.ORGANIZATIONS, CoreResourceType.USERS,
    ResourceRelationshipCardinality.MANY, ResourceRelationshipCardinality.MANY,
    {
        jointResourceType: CoreResourceType.ORGANIZATION_MEMBERS,
        accessScopeStrategy: ResourceRelationshipAccessScopeStrategy.STANDALONE,
        nature: RelationshipNature.ASSOCIATION,
        contextPolicy: {
            objectMode: ContextPolicy_ObjectMode.SUMMARY,
            operationOverrides: {
                [CoreResourceOperation.LIST]: { enabled: false },
                [CoreResourceOperation.SEARCH]: { enabled: false },
            }
        }
    }
),

jointResourceType identifies the membership resource that carries each connection’s fields and operations. The registered association supplies the peer expansion; merely adding organization and user IDs to an unrelated schema does not.

Do not reuse this single many-to-many declaration with the same task type at both ends. Self-referencing peer expansion is refused; the explicit source and target edges above describe the junction record directly and preserve which foreign key means what.

Put connection-specific facts on the junction. A person’s organization role belongs to membership, while a task’s priority belongs to the task. Give the junction its own operations and lifecycle where people need to create, change or remove the connection.

A relationship type named “depends on” records that meaning; any scheduling or completion rule that acts on it belongs to the application’s behavior. The schema above also checks that the two task IDs differ. It does not, by itself, define a complete scheduling algorithm.

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.