@limova-org/custom-tools
v0.9.0
Published
Limova custom LangChain tool specifications and adapters
Readme
@limova-org/custom-tools
Declarative tool specification catalog for Limova AI agents. This package defines what agents can do without containing any execution logic.
Role in the architecture
custom-tools/ api/ (NestJS)
ToolSpecV1 definitions ToolRegistry
(schemas, descriptions, → PipedreamToolFactory
Pipedream actions) ToolExecutionService
PropsMappers PipedreamActionRunner
LangChain adapters LLM (Claude, Gemini)This package is a contract between AI agents and third-party integrations:
- For the LLM: each spec provides a
name,description, andinputSchema(JSON Schema) that the model uses to decide when and how to call a tool - For Pipedream: each spec maps to one or more
pipedreamActions(e.g.gmail-send-email) executed on the API side - For the API: adapters (
buildPipedreamTool,buildElevenLabsWebhookTool) convert specs into LangChainDynamicStructuredToolinstances
The package has zero runtime dependencies (everything is in peerDependencies). It only exports types, immutable objects, and pure functions.
Run pnpm validate to see the current tool count and domain breakdown.
Key concepts
ToolSpecV1
The fundamental unit. Every tool is an immutable (readonly) object:
interface ToolSpecV1 {
name: string; // e.g. "email_send"
domain: ToolDomain; // e.g. "email"
description: string; // LLM-facing description
inputSchema: JsonSchemaObjectType; // tool parameters (JSON Schema)
providerScoped: boolean; // true if multi-provider (Gmail + Outlook)
providers?: string[]; // ["gmail", "microsoft_outlook"]
pipedreamActions?: Record<string, string>; // { gmail: "gmail-send-email" }
}Domains
Tools are organized by domain. Three categories exist:
- Pipedream integrations: airtable, brevo, canva, google-sheets, hubspot, notion, shopify, slack, whatsapp-business, etc.
- Abstract multi-provider domains:
email(Gmail/Outlook),calendar(Google Calendar/Outlook) - Internal tools:
code-interpreter,file-system,retrieval(RAG),shared-memory,date
The full list of domains and tool names is in TOOL_INVENTORY (src/inventory.ts).
PropsMappers
Pure functions that transform LLM arguments (unified schema) into Pipedream-specific props:
LLM: { to: ["[email protected]"], subject: "Hello", body: "..." }
↓ sendMapper(args, "gmail")
Pipedream: { to: "[email protected]", subject: "Hello", body: "...", bodyType: "html" }
↓ sendMapper(args, "microsoft_outlook")
Pipedream: { recipients: "[email protected]", content: "...", contentType: "html" }TOOL_INVENTORY
Static manifest (Record<ToolDomain, string[]>) exhaustively listing all tool names per domain. Automatically validated against the registry in CI (pnpm validate) to ensure consistency.
Structure
src/
index.ts # Entry point, side-effect imports + re-exports
registry.ts # Map<RegistryKey, ToolSpecV1> singleton
inventory.ts # TOOL_INVENTORY (static manifest)
types/
tool-spec.ts # ToolSpecV1, JsonSchema*, PhoneAgentWebhookSpec
adapter.types.ts # PropsMapper, PipedreamActionRunner, ToolHandler
registry.types.ts # RegistryKey, ResolveContext
adapters/
langchain.ts # buildLangchainTool, buildPipedreamTool
elevenlabs.ts # buildElevenLabsWebhookTool (phone agent)
utils/
json-schema-to-zod.ts # JSON Schema → Zod conversion for LangChain
integrations/ # Legacy specs (12 domains: email, calendar, slack...)
specs/ # Self-registering specs (35+ domains with register.ts)
<provider>/
register.ts # Side-effect registration
<tool-name>.ts # Individual spec
mappers/ # PropsMappers for this providerConsumption in the API
The package is installed via an npm alias:
"@limova/custom-tools": "npm:@limova-org/[email protected]"The API imports it as @limova/custom-tools and wraps it behind ToolSpecCatalogService (NestJS facade).
Adding a new integration (end-to-end guide)
This is the full workflow to add tools for a new Pipedream provider (e.g. acme). Use an existing provider like brevo/ or axonaut/ as reference.
Step 1 — Find the Pipedream action IDs
Look up available actions for the provider on the Pipedream registry. Each action has an ID like acme-create-contact. You'll need these IDs for the pipedreamActions field.
You can also fetch them programmatically:
# List all available actions for a component
curl -s "https://api.github.com/repos/PipedreamHQ/pipedream/contents/components/acme/actions"
# Get the detailed schema of a specific action (props, types, required fields)
curl -s \
-H "Authorization: Bearer <pipedream-registry-token>" \
"https://api.pipedream.com/v1/components/registry/acme-create-contact"The registry response contains configurable_props with prop names, types, descriptions, and required/optional status — use this as the source of truth for your inputSchema.
Step 2 — Create the spec files
Create a folder src/specs/acme/ with one file per tool:
// src/specs/acme/create-contact.ts
import type { ToolSpecV1 } from '@/types/tool-spec';
export const acmeCreateContactSpec: ToolSpecV1 = {
name: 'acme_create_contact',
domain: 'acme',
description: 'Create a new contact in Acme CRM.',
providerScoped: false,
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Email address of the contact.',
},
name: {
type: 'string',
description: 'Full name of the contact.',
},
},
required: ['email'] as const,
},
tags: ['acme', 'crm', 'contact', 'create'],
pipedreamActions: {
acme: 'acme-create-contact',
},
};Key rules:
namefollows the pattern<domain>_<action>(snake_case)descriptionis LLM-facing — be clear and concise about what the tool does- Every property in
inputSchemamust have adescription pipedreamActionsmaps the provider slug to the Pipedream action ID
Step 3 — Create the register file
// src/specs/acme/register.ts
import { registerToolSpec } from '@/registry';
import { acmeCreateContactSpec } from './create-contact';
const ACME_SPECS = [
acmeCreateContactSpec,
] as const;
ACME_SPECS.forEach(registerToolSpec);Step 4 — Wire the side-effect import
In src/index.ts, add the import at the top with the other register imports (alphabetical order):
import './specs/acme/register';Then add re-exports for the spec constants at the bottom:
export { acmeCreateContactSpec } from './specs/acme/create-contact';Step 5 — Add to TOOL_INVENTORY
In src/inventory.ts, add the new domain entry (alphabetical order):
acme: [
'acme_create_contact',
],Step 6 — Add a PropsMapper (if needed)
If the Pipedream action expects props in a different format than the inputSchema, create a mapper:
// src/specs/acme/mappers/create-contact.mapper.ts
import type { PropsMapper } from '@/types/adapter.types';
export const acmeCreateContactMapper: PropsMapper = (args, _provider) => {
return {
...args,
// transform args to match Pipedream's expected props
};
};Export it from src/index.ts:
export { acmeCreateContactMapper } from './specs/acme/mappers/create-contact.mapper';Step 7 — Validate and test
pnpm build && pnpm validate && pnpm testpnpm validate checks that every name in TOOL_INVENTORY has a matching spec in the registry, and vice versa.
Step 8 — Verify against Pipedream docs
Before publishing, verify your implementation against the official Pipedream registry. For each tool, fetch its schema and compare:
# Verify action ID exists and get its props
curl -s \
-H "Authorization: Bearer <pipedream-registry-token>" \
"https://api.pipedream.com/v1/components/registry/acme-create-contact"Check that:
- Action IDs in
pipedreamActionsexist in the registry - Prop names and types match
configurable_props - Required vs optional fields are correct
Fix any mismatches before publishing.
Step 9 — Publish
git add . && git commit -m "feat: add acme integration"
git push origin mainThe CI will automatically publish a new version to npm.
Step 10 — Install in the API
After the npm release, update the dependency in the API:
cd ../api
pnpm update @limova-org/custom-tools @limova/custom-toolsIf the tools need a PropsMapper, wire it in api/src/modules/agentic/tools/pipedream/constants/pipedream-props-mappers.constants.ts:
import { acmeCreateContactMapper } from '@limova/custom-tools';
export const PIPEDREAM_PROPS_MAPPERS: PropsMappersByToolName = {
acme_create_contact: acmeCreateContactMapper,
// ...existing mappers
};If the tools need dynamic props resolution (Pipedream reloadProps), add the spec names to SPECS_REQUIRING_DYNAMIC_PROPS in api/src/modules/agentic/tools/pipedream/constants/pipedream-tool-factory.constants.ts.
Step 11 — Test end-to-end
Test the integration by sending a message through the Limova chat that triggers the new tools. Verify that the tool calls execute successfully and return the expected results.
Step 12 — Assign tools to an agent
In the database, add the tool names to the agent's tools JSONB array. The tools will be loaded automatically on the next agent reload (POST /agent/reload).
Common pitfalls
PropsMapper must have 2+ parameters. The API uses isPropsMapper() which checks function.length >= 2. A mapper with only (args) will be silently ignored. Always use (args, _provider).
{ type: 'object' } without properties. If an inputSchema property is { type: 'object' } with no properties field (e.g. a dynamic fields or data object), jsonSchemaToZod converts it to z.record(z.string(), z.unknown()). If you accidentally add an empty properties: {}, it becomes z.object({}) which strips all dynamic keys silently at runtime.
Dynamic props (reloadProps). Some Pipedream actions use additionalProps() / reloadProps: true to resolve their schema dynamically (e.g. Airtable fields, Brevo lists). These specs must be added to SPECS_REQUIRING_DYNAMIC_PROPS in the API, otherwise the props won't be resolved and the action will fail.
Provider slug mismatches. Some providers have special slugs in Pipedream. For example, Slack uses slack_v2 as the app slug but action IDs use the slack-* prefix (not slack_v2-*). Check APP_PROP_NAME_OVERRIDES in the API if auth fails with not_authed.
Local development tip: use
pnpm local-libs:refreshinapi/to test changes without publishing to npm. Usepnpm start:dev:local-libsfor hot-reload across both packages.
Setup
pnpm installScripts
pnpm build # Build with tsup (ESM + CJS + types)
pnpm lint # ESLint fix
pnpm typecheck # TypeScript check
pnpm test # Run tests (vitest)
pnpm validate # Validate TOOL_INVENTORY vs registryRelease and npm publishing
How it works
The project uses release-it with the @release-it/conventional-changelog plugin to automate releases.
On every push to main, the CI:
- Runs build, validate, lint, typecheck, tests
- Analyzes commits since the last tag
- Determines the version bump based on conventional commits
- Updates
package.jsonandCHANGELOG.md - Creates a release commit, a git tag, and a GitHub Release
- Publishes to npm
If there are no triggering commits (fix: or feat:), the release step does nothing.
Conventional commits
| Prefix | Release | Bump | Example |
|--------|---------|------|---------|
| fix: | Yes | patch (0.0.x) | fix: correct schema validation |
| feat: | Yes | minor (0.x.0) | feat: add slack integration |
| feat!: | Yes | major (x.0.0) | feat!: rename tool spec format |
| chore: | No | - | chore: update dependencies |
| docs: | No | - | docs: update README |
| refactor: | No | - | refactor: simplify adapter logic |
| test: | No | - | test: add missing unit tests |
A
BREAKING CHANGE:in the commit body also triggers a major bump.
Manual release (optional)
To trigger a release locally (interactive mode):
pnpm releaseConfiguration
.release-it.json— release-it configuration.github/workflows/release.yml— CI workflow
npm configuration (one-time setup)
Billing prerequisites
To publish private packages under the @limova-org scope, you need:
- A paid plan on the
limova-orgnpm organization (npm Teams) - A paid plan on the publisher's personal npm account
npm requires both plans for private scoped packages. Without the personal paid plan, publish fails with
E402 Payment Required - You must sign up for private packages, even if the organization is on a paid plan.
npm token
- Go to npmjs.com > avatar > Access Tokens > Generate New Token
- Choose Granular Access Token
- Configure:
- Organizations: select
limova-org - Packages: "All packages" or specific packages, permissions Read and write
- Bypass two-factor authentication (2FA): check (required for CI)
- Organizations: select
- Copy the token
GitHub Actions secret
- In the GitHub repo > Settings > Secrets and variables > Actions > New repository secret
- Name:
NPM_TOKEN, value: the Granular Access Token
First publish
The first publish of a new package must be done manually locally:
npm login
npm publishSubsequent publications are handled automatically by the CI.
Important: the
@limova-orgscope matches the exact npm organization name. If the org name changes, update thenamefield inpackage.json.
