Skip to main content
Wildo.ai Coming soon

Internal tools & business software

An application tailored to how your teams manage records, make decisions and work with the ERP and CRM they already run.

Read the film instead

A brief for a Maintenance request: Starts, an employee submits; Roles, coordinator, technician, manager; Done, the outcome is recorded. It becomes an ordinary application: its screens, its actions, its roles. There is no special runtime. "Give your team a better way to work." "An internal tool is your application, shaped by your brief."

The team works from a list: Status: Open, with a badge counting what needs them, Assigned to me · 3. Two rows are selected — Close selected — and Export to CSV keeps the same filters. "A working surface for every day." "Filter, act on several at once, take the list with you."

A coordinator uses Assign, a named action. Two checks run: Who may assign — a coordinator; Who may be assigned — an active member with the right role. Assigning someone without that role: Refused. The action's name decides nothing; its declaration does. "An action with its responsibilities." "Who may act, and on whom, is part of the action."

People join deliberately: Invitation only, or through the company's Single sign-on. With directory provisioning, the directory adds Sam, and a Department becomes a team. Sam leaves: access ends. "People join and leave deliberately." "The directory stays the source of who works here."

The company is organised in teams: Facilities, with North and South. A North technician sees North's requests; South's stay closed to them. "Each team sees its own work." "Teams narrow what each person reaches, inside one organization."

The company's ERP joins as a system of record: systemsOfRecord, odoo. Billable services are Read through: each read asks Odoo. External customers are a Local copy, synchronised every hour, with a local field of the team's own, Internal note. "Use what your company already knows." "Decide which system owns each piece of information."

The work is followed through: Requests by status, Requests by priority, a Notification to the manager, a Work order PDF attached to the record, and an audit row — Assigned, by a coordinator, field: assignee. "Follow the work through." "Measures, notices, documents and a record of who did what."

Acceptance follows one staff journey end to end: An allowed action succeeds; a refused action is refused; after a role change, access follows; when the source system is unavailable, the error is visible. "Accept it the way the team will use it." "Test the refusals as carefully as the successes."

The painting from this page: People, Responsibilities and Systems feeding a Team workspace, where work moves to Review, Decide and Follow through. "A shared place for the work that keeps your company moving." Wildo.ai

> Work organized around your operations > Responsibilities built into each action > Existing systems brought into context

An internal tool gives your staff a shared place to manage records, make decisions and follow work through to completion.

Wildo provides the application structure and common mechanisms. You define the work, the people responsible for it and the connections to the systems your company already uses.

Built around the way your organization works

Make the next step clear

Bring requests, assignments and decisions into an application shaped around your process. Give people the information and actions they need to move work forward.

Give responsibility a clear place

Connect access to people’s roles and the context of the work. Separate who can see a record, who can change it and who may make a particular decision.

Use what your company already knows

Work with the records in Odoo, SAP S/4HANA, Salesforce, Dynamics 365, NetSuite or any OData service: read them in place, write changes back where the system allows it, keep a local copy current or keep both sides in step, with each field’s owner declared.

Example: A service desk for a facilities team

Employees submit maintenance requests. Coordinators assign work, technicians record the outcome and managers review exceptions. An equipment catalogue comes from an existing system; the tool owns the request and its follow-up.

For engineers

Start with the work and its boundaries

An internal tool uses Wildo’s application foundation: frontend, backend API, shared model, specifications and registered modules. Its purpose comes from the operational brief, rather than a separate internal-tool runtime type.

Describe the work item, its starting event, responsible roles, decisions and completion condition. Identify which system owns each piece of information. Choose application, organization or user scope for the actual records; a staff audience does not automatically make all records application-wide.

DecisionWhere it becomes executableExample question
Who entersUser-type acquisition and authentication configurationAre users invited or provisioned by an administrator?
Where work belongsResource scope and relationshipsIs this request shared across the company or restricted to a team?
What people may doOperation variants and backend behaviorWho can assign, approve or close it?
Where information comes fromLocal resource, system-of-record binding or pipelineIs the equipment catalogue read in the ERP, changed there or copied locally?
What people seeNavigation, resource UI behavior and custom viewsDoes a technician need a focused work screen?

Keep the project useful beyond its first delivery

The normal wildo init path creates the application starting structure. Add the operational model and experience to that source project. Keep specifications and executable declarations together as responsibilities, systems and workflows change.

Use wildo setup for the authored local environment, wildo local init for local platform initialization and registration, and wildo local dev for development processes, after installing the prerequisites. Against an already healthy shared platform, registration can use wildo local init --register-only.

Commercial subscriptions are a product choice, not the definition of an internal tool. Select the mechanisms this application needs and verify their configuration; do not infer a special billing-free generation path from the audience label.

Accept a complete staff journey

Verify one work item from entry to completion with different people. Exercise an allowed action and its refused counterpart through the API, then change the person’s role and repeat. Check external-system failure, visible error handling and the expected record state after retrying.

Deliver the source, environment configuration and operational instructions alongside that journey. A working screen alone does not establish the access boundary or the behavior of an integration.

Bring work from request to outcome

Organize records around the work people need to complete. Give each step a clear action, useful context and an identifiable result.

Shared declarations support standard forms and APIs. Your business rules and custom screens make those mechanisms fit the way your team works.

A work request connects its owner, status and history with review, approval and completion.

Make progress visible and actionable

Describe the work

Capture the information needed for a decision, with field rules shared by standard forms and request validation.

Name the next action

Use actions such as assign, approve or close to express intent. Give each its own inputs, access rules and behavior.

Keep the context nearby

Bring related records, attachments and configured views into the working experience so people can act with the information they need.

Example: Assign a request to someone who can handle it

A coordinator selects a responsible person. The application checks both the coordinator’s right to assign and the selected person’s eligibility before changing the request.

For engineers

Separate the caller from the person being assigned

Wonder Todos demonstrates this distinction in its ASSIGN_LEAD operation. This selected entry comes from its operation map; imports and the surrounding resource declaration are omitted.

[Todos_Operations.ASSIGN_LEAD]: {
  variants: [{
    variantType: ResourceOperationVariantType.API_CALL,
    isDefault: true,
    roles: [CORE_ORG_ROLES.ORG_ADMIN],
    riskLevel: ResourceOperationRiskLevel.MEDIUM,
    resourceOperationLike: CoreResourceOperation.UPDATE,
    requestDto: z.object({
      assignedToUserId: z.string().min(1),
    }),
    referenceConstraints: {
      assignedToUserId: {
        qualifyingStatuses: [OrganizationMemberStatus.ACTIVE],
        requiredRoles: {
          scope: ResourcePrimaryScope.ORGANIZATIONS,
          roles: [CORE_ORG_ROLES.ORG_ADMIN],
        },
      },
    },
  }],
},

roles restricts the caller. referenceConstraints restricts the assignee to an active member with the required organization role. The existing update mechanism performs the change; the registered field and relationship give that reference its meaning.

An internal tool can use the same pattern with its own roles and actions. Approval thresholds, transitions and external checks need the corresponding application-authored behavior. The name approve does not create an approval policy.

Shape the working surface

Resource UI behavior chooses how operations appear, including addressable and embedded surfaces. Custom views can combine standard layouts with resource-owned views. Keep these presentation choices separate from backend permission checks.

Work concernAuthoring responsibilityAcceptance check
Required informationSchema and request contractMissing or invalid input is refused
Eligible actorOperation roles and contextAn unauthorized caller cannot act
Valid targetRelationship and reference constraintsAn ineligible assignee is refused
Business transitionCustom behavior where neededThe wrong prior state cannot be advanced
Visible resultRecord view and feedbackThe updated owner or outcome is clear

Choose which changes need auditable events and configure or emit them through the relevant mechanisms. Verify the actual event and actor attribution; a visible activity panel does not itself prove every custom action is recorded.

Give people access that matches their responsibility

Bring staff into the application through a deliberate entry policy. Then connect their roles to the records and actions they need.

Signing in, joining a team and approving a request are separate decisions. Keeping them distinct makes the tool easier to operate as people join, move or leave.

Requester, reviewer and administrator have distinct responsibilities.

Put responsibility into everyday use

Choose how people enter

Use the configured acquisition paths for invitations or administrative provisioning, with authentication suited to your organization.

Scope the work

Choose which records belong to the company, a workspace or a person. Reflect that context in queries and operations.

Keep permissions current

Make membership and role changes part of operating the tool. Verify their effect on backend access as well as the interface.

Example: A reviewer moves to another team

The administrator updates membership and responsibilities. The reviewer can work in the new context, while requests from the old team remain subject to their own scope and access rules.

For engineers

Declare how a user type may be acquired

The SaaS skeleton declares registration inside userTypes.member.auth. This illustrative replacement for that registration block chooses invitation-only acquisition. The enums are exported by @wildo-ai/saas-models; surrounding authentication settings remain application-owned.

registration: {
  mode: RegistrationMode.INVITATION_ONLY,
  allowedMethods: [AuthMethod.PASSWORD],
  emailVerification: EmailVerificationMode.REQUIRED_BEFORE_ACCESS,
  blockForSSODomains: false,
},

The acquisition policy accepts invitation and administrative-provisioning channels in this mode. It does not admit self-registration or SSO just-in-time acquisition. Existing holders of a user type are evaluated differently from people acquiring it for the first time: acquisition policy is not a blanket login prohibition.

allowedMethods governs registration methods; it does not replace the configured authentication methods or MFA policy. Configure the invitation messages and their provider where invitations are part of the chosen journey.

Model organizational context deliberately

User types, organization memberships and operation roles have different jobs. Use an organization where it represents a real boundary. Declare resource scopes and relationships so reads and mutations carry that boundary into execution.

LayerEstablishesDoes not establish by itself
AuthenticationThe signing-in identityPermission to act on every record
Acquisition policyHow a user type is first obtainedMembership in every workspace
Membership and roleA person’s position in a contextAutomatic partitioning of custom resources
Resource and operation policyAccess to the selected workA business transition merely because its button exists

Verify the joiner, mover and leaver journeys

Test invitation or provisioning, first access, membership change and removal. Check direct API calls after a role changes. For SSO, exercise the selected provider and existing-user versus new-user paths separately; domain configuration and acquisition policy must agree.

Read users, organizations and security for the underlying identity and authorization mechanisms.

Bring existing information into the work

Most internal tools are built on systems the company already runs: an ERP for products, invoices and companies, a CRM for accounts and contacts. Wildo connects a tool to them by name — Odoo, SAP S/4HANA, Salesforce, Microsoft Dynamics 365 (Dataverse), Business Central, NetSuite, or any service that speaks OData version 4 — and lets each kind of record be used in one of four ways.

Read the records where they live, and change them there when you allow it. Keep a local copy that stays current. Send the records the tool creates to the other system. Or keep one record in step on both sides, with each field’s owner declared. Each choice decides where the record lives, how fresh it is and who may change it, so decide which system owns each piece of information before adding an integration.

An internal tool uses its ERP or CRM through four distinct paths: read in place, a local copy, writes back, and two-way exchange.

Connect systems with a clear purpose

Work in the system that owns the record

A read-through resource shows the other system’s records in the tool’s screens and API without copying them. Where you open them, edits, new records and deletions are sent there once, under the tool’s own service account, and recorded; declared actions, such as posting an invoice, run in the ERP itself.

Keep a copy where the tool needs one

A pipeline copies selected fields into the tool’s own database, where people add notes, files and relationships. It can read only what changed since its last pass, re-read a record when the other system announces a change, and keep a record erased from the tool from being copied back.

Decide which side owns each field

The same declaration can send the records the tool creates, or keep one record in step on both sides. Each field is declared as the other system’s, the tool’s or shared, and a shared field changed on both sides becomes a conflict that the declared policy or an operator settles.

Example: Equipment comes from another system

A facilities tool reads the equipment catalogue from the company’s ERP. Maintenance requests remain local and refer to the relevant equipment; when a technician corrects an item’s location in the tool, the correction is written to the ERP, which stays the catalogue. If the tool needs its own copy for notes or attachments, a pipeline keeps that copy current.

For engineers

Declare the system once

Wonder Todos declares its Odoo instance once, as a named system of record in wildo.saas.config.ts. This excerpt omits its comments and its privacy-notice declaration:

systemsOfRecord: {
  odoo: {
    vendor: 'odoo',
    dialect: HttpApiTransportDialect.ODOO_JSONRPC,
    partition: true,
  },
},

The name odoo identifies the instance; resources bind it with system: 'odoo', and a second deployment would be a second name. Each environment supplies the address and the service account’s credential under names derived from that name, so nothing about access is committed in a resource. An environment without access declares the instance absent: the tool still starts, and reads of bound resources are refused rather than answered empty.

partition: true says this Odoo holds several companies. Rows are then confined to the organization units an operator assigns each Odoo company to, so a subsidiary’s staff see and change only their own company’s records; a record that belongs to no company is shared by everyone. This confinement exists for Odoo only.

What each vendor supports differs, and startup refuses a declaration the vendor cannot honour rather than letting it fail at the first request:

System (vendor)Read in placeChange from the toolCopy kept currentSend, or keep in stepWhat to know
Odoo (odoo)YesCreate, update, delete; model methods such as posting an invoiceFull or changed-only passes; re-read on notificationBothPer-company confinement; the check before an update narrows a lost-update race but cannot close it
SAP S/4HANA (sap-s4hana)OData version 4 and version 2 servicesNoYes; changed-only passes and re-reads on version 4 servicesNoWrites are refused at startup: SAP requires a token handshake the engine does not perform; sign-in by communication-user password only
Salesforce (salesforce)SOQL queriesCreate, update, deleteFull passes walked by keySend onlyLast writer wins: Salesforce offers no record version to check
NetSuite (netsuite)SuiteQL queriesCreate, update, deleteFull passes walked by keySend onlyLast writer wins: no record version to check
Microsoft Dataverse (microsoft-dataverse)YesCreate, update, delete; an update can be checked against the record’s versionFull or changed-only passes; re-read on notificationBothEvery business unit the service account can see; a deep list costs one request per earlier page
Business Central (microsoft-dynamics-365-business-central)YesCreate, update, delete; an update can be version-checkedFull or changed-only passes; re-read on notificationBothEvery company the service account can see, unless the address names one company
Any OData v4 service (odata)YesCreate, update, delete, with optional version check; bound and unbound actionsFull or changed-only passes; re-read on notificationBothNo vendor-specific corrections; assumes the service accepts $skip paging

Sending, in either sending direction, accepts only records that hold no personal data, because no erasure reaches a record once it has been sent. Every vendor’s reads, writes and pipelines are tested against simulators of its protocol. Odoo is the only one the dogfood application declares, and the shape of its company field was measured on a live Odoo; no vendor has been driven end to end against a live tenant.

Read and change records in place

The billable-services resource binds that instance by name. This selected configuration excerpt shows identity, field mapping and the writes it opens; source comments, the schema, relationships and operation declarations are omitted.

persistenceAdapter: PersistenceAdapter.HTTP_API,
httpApiBinding: {
  dialect: HttpApiTransportDialect.ODOO_JSONRPC,
  system: 'odoo',
  entityRef: 'product.template',
  keyFields: [{ localField: 'odooProductId', remoteField: 'id', codec: HttpApiKeyComponentCodec.INTEGER }],
  fieldMappings: [
    { localField: 'listPrice', remoteField: 'list_price' },
    { localField: 'unitOfMeasure', remoteField: 'uom_name' },
    { localField: 'lastUpdatedAt', remoteField: 'write_date' },
  ],
  subjectRights: { posture: SystemOfRecordSubjectRightsPosture.NO_SUBJECT_DATA },
  writableVerbs: [HttpApiWriteVerb.CREATE, HttpApiWriteVerb.UPDATE, HttpApiWriteVerb.DELETE],
  writeConcurrency: { mode: HttpApiWriteConcurrencyMode.VERSION_CHECKED, versionField: 'lastUpdatedAt' },
},

Remote field names are translated into the local schema, while the key mapping supplies stable record identity. writableVerbs opens creates, updates and deletes, each matched by a declared operation. Every write requires an idempotency key and is sent once: an interrupted write is reported as an unknown outcome rather than repeated. VERSION_CHECKED compares the write_date the caller read with Odoo’s own just before an update and refuses a stale one. A no-subject-data erasure declaration is only appropriate when the resource actually has that property; a product catalogue does.

An ERP operation that is not a row write, such as posting an invoice, is declared as a named action on the binding and runs through its own executor, with its own roles and audit record. Wonder Todos’ customer-invoices resource declares one; see actions in a system of record.

Keep a copy, in either direction

The external-customers resource keeps a local copy of Odoo’s client accounts. Source comments are omitted:

externalDataPipeline: {
  binding: {
    dialect: HttpApiTransportDialect.ODOO_JSONRPC,
    system: 'odoo',
    entityRef: 'res.partner',
    keyFields: [{ localField: 'id', remoteField: 'id', codec: HttpApiKeyComponentCodec.INTEGER }],
    subjectRights: { posture: SystemOfRecordSubjectRightsPosture.OWNED_COPY },
    purposeRefs: ['task-planning-service'],
  },
  remoteKeyLocalField: 'odooPartnerRef',
  partitionUnitLocalField: 'organizationUnitId',
  partitionKeyLocalField: 'odooCompanyRef',
  mapping: [
    {
      kind: ExternalDataMappingEntryKind.CONCATENATION,
      localField: 'name',
      parts: [
        { kind: ExternalDataConcatenationPartKind.REMOTE_FIELD, remoteField: 'name' },
        { kind: ExternalDataConcatenationPartKind.REMOTE_FIELD, remoteField: 'ref' },
      ],
      separator: ' · ',
    },
    { kind: ExternalDataMappingEntryKind.REMOTE_FIELD, remoteField: 'email', localField: 'email' },
  ],
  sourceFilter: { is_company: true },
  population: ExternalDataDestinationPopulation.CLOSED,
  orphanPolicy: ExternalDataOrphanPolicy.IMPERSONALIZE,
  scheduleCron: '0 * * * *',
},

The subject-rights posture says the tool keeps this copy itself: it holds the team’s notes, which Odoo does not, so its own erasure applies to the copy, while Odoo answers requests about the records it came from. Because a copied name and email can describe a person, NO_SUBJECT_DATA would be refused. The two partition fields are written by the engine: the organization unit the client’s Odoo company is assigned to, and Odoo’s own company id. The CONCATENATION entry is a declared expression that shows the client’s name followed by its finance reference. IMPERSONALIZE scrubs the name, email and note of a client that no longer appears in a complete pass, and keeps the row; it writes no suppression, so a record Odoo lists again loads as a new row. An erasure through the tool scrubs the row the same way and also records a suppression, so the next pass does not copy the client back.

The same declaration takes a direction. An outbound pipeline sends the rows the tool creates and changes, writing back the key the other system answers; a two-way pipeline declares each mapped field’s owner — the other system’s, the tool’s or both — and a conflict policy for a shared field changed on both sides. Neither sends a local delete. See sending local changes and two-way sync with field ownership.

Choose the storage consequence

ChoiceWhere rows liveDesign consequence
HTTP_API with httpApiBindingRemote systemReads and opened writes depend on the remote service; there is no local row to enrich
Local resource with an inbound externalDataPipelineApplication storeFreshness depends on successful runs; mapped fields stay the source’s
Outbound pipelineApplication store, sent to the remote systemThe tool owns the mapped fields; a remote change is not read back
Two-way pipelineBothEach field has a declared owner; conflicts on shared fields need a policy

A pipeline needs its own extraction, mapping and load contract. Do not infer successful synchronization from the existence of a resource declaration or copy a sample without checking its current integration status.

Make failure understandable to staff

Define what the user sees when the source is unavailable and how stale local data is identified. Verify authentication failure, pagination, mapping and repeat runs. A partial import must not be treated as a complete source inventory when deciding which existing rows to remove; the engine concludes that a record disappeared only after a completed pass.

Operators reach pipelines through an admin API behind the application’s admin secret: each pipeline’s health and its unreachable-source streak, a run on demand, the run history with the rows a run rejected and why, a reset, and the answers to conflicts and unknown outcomes. There is no operator screen; locally, wildo context pipelines reads the same listing. For writes, decide what staff see when an outcome is unknown, and which roles may change records in the other system: the tool’s permissions decide who may ask, and the service account’s permissions at the vendor decide what can be done.

A shared place for the work that keeps your company moving.

An internal tool connects information, responsibility and action. Wildo supplies a common application foundation; your operational knowledge gives it purpose.

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.