@memberjunction/actions-bizapps-crm
v5.21.0
Published
CRM system integration actions for MemberJunction
Readme
@memberjunction/actions-bizapps-crm
Customer Relationship Management (CRM) integration actions for MemberJunction. This package provides 22 HubSpot CRM actions covering contact, company, deal, and activity management, built on a provider-extensible architecture that supports multi-tenant credential management.
This package is part of the BizApps actions family within the MemberJunction Actions Framework. See those documents for framework-level patterns, the action design philosophy, and shared conventions across all BizApps integrations.
Architecture
The package uses a three-tier class hierarchy: a domain-level base class for all CRM providers, a provider-specific base class for HubSpot, and individual action classes for each operation.
graph TD
subgraph Framework["MemberJunction Actions Framework"]
BA["BaseAction<br/>@memberjunction/actions"]
end
subgraph CRM["CRM Domain Layer"]
BCRM["BaseCRMAction<br/>Credential management, validation helpers,<br/>date/phone formatting, status mapping"]
end
subgraph HubSpot["HubSpot Provider Layer"]
HSB["HubSpotBaseAction<br/>API auth, pagination,<br/>search, batch ops, associations"]
end
subgraph Actions["HubSpot Actions (22 total)"]
direction LR
CC["Contact Actions<br/>Create, Update, Get,<br/>Search, Delete, Merge"]
CO["Company Actions<br/>Create, Update, Get,<br/>Search, Associate"]
DA["Deal Actions<br/>Create, Update, Get,<br/>Search, By Contact, By Company"]
AA["Activity Actions<br/>Log Activity, Create Task,<br/>Update Task, Get Activities,<br/>Get Upcoming Tasks"]
end
BA --> BCRM
BCRM --> HSB
HSB --> CC
HSB --> CO
HSB --> DA
HSB --> AA
style Framework fill:#64748b,stroke:#475569,color:#fff
style CRM fill:#2d6a9f,stroke:#1a4971,color:#fff
style HubSpot fill:#b8762f,stroke:#8a5722,color:#fff
style Actions fill:#2d8659,stroke:#1a5c3a,color:#fffCredential Resolution Flow
All actions resolve credentials through a two-step lookup: environment variables first, database fallback second. This supports both development and production deployment patterns.
flowchart LR
A["Action receives<br/>CompanyID param"] --> B["Look up<br/>CompanyIntegration<br/>entity"]
B --> C{"Env var<br/>exists?"}
C -->|Yes| D["Use env var<br/>BIZAPPS_HUBSPOT_<br/>{ID}_ACCESS_TOKEN"]
C -->|No| E{"DB credentials<br/>exist?"}
E -->|Yes| F["Use AccessToken /<br/>APIKey from entity"]
E -->|No| G["Throw error:<br/>no credentials"]
style A fill:#64748b,stroke:#475569,color:#fff
style B fill:#2d6a9f,stroke:#1a4971,color:#fff
style C fill:#b8762f,stroke:#8a5722,color:#fff
style D fill:#2d8659,stroke:#1a5c3a,color:#fff
style E fill:#b8762f,stroke:#8a5722,color:#fff
style F fill:#2d8659,stroke:#1a5c3a,color:#fff
style G fill:#7c5295,stroke:#563a6b,color:#fffInstallation
npm install @memberjunction/actions-bizapps-crmThis package is server-side only. It is typically consumed by MJAPI or other server-side hosts that register actions via the MemberJunction class factory.
Configuration
1. Create Integration Record
Register HubSpot as an integration in your MemberJunction database:
INSERT INTO Integration (Name, Description, NavigationBaseURL, ClassName)
VALUES ('HubSpot', 'HubSpot CRM Integration',
'https://api.hubapi.com', 'HubSpotIntegration');2. Link to Company
Create a CompanyIntegration record for each tenant/company that uses HubSpot:
INSERT INTO CompanyIntegration (CompanyID, IntegrationID, ExternalSystemID, IsActive)
VALUES (@CompanyID, @HubSpotIntegrationID, @HubSpotPortalID, 1);3. Set Credentials
Credentials are resolved from environment variables first, then from database fields.
Environment variables (recommended):
# OAuth access token (preferred)
BIZAPPS_HUBSPOT_{COMPANY_ID}_ACCESS_TOKEN=your_access_token
# Or private app API key
BIZAPPS_HUBSPOT_{COMPANY_ID}_API_KEY=your_api_key
# Optional OAuth fields
BIZAPPS_HUBSPOT_{COMPANY_ID}_REFRESH_TOKEN=your_refresh_token
BIZAPPS_HUBSPOT_{COMPANY_ID}_CLIENT_ID=your_client_id
BIZAPPS_HUBSPOT_{COMPANY_ID}_CLIENT_SECRET=your_client_secretDatabase fallback: store credentials in the AccessToken, APIKey, or RefreshToken columns of the CompanyIntegration entity.
Available Actions
Contact Management
| Action | Class | Description |
|--------|-------|-------------|
| Create Contact | CreateContactAction | Creates a new contact with email validation and optional company association |
| Update Contact | UpdateContactAction | Updates an existing contact's properties |
| Get Contact | GetContactAction | Retrieves a single contact by ID with optional associations |
| Search Contacts | SearchContactsAction | Searches contacts with filters, sorting, pagination, and date ranges |
| Delete Contact | DeleteContactAction | Deletes (archives) a contact |
| Merge Contacts | MergeContactsAction | Merges duplicate contacts into a single record |
Company Management
| Action | Class | Description |
|--------|-------|-------------|
| Create Company | CreateCompanyAction | Creates a new company record |
| Update Company | UpdateCompanyAction | Updates an existing company's properties |
| Get Company | GetCompanyAction | Retrieves a single company by ID |
| Search Companies | SearchCompaniesAction | Searches companies with flexible filter criteria |
| Associate Contact to Company | AssociateContactToCompanyAction | Links contacts to companies with batch support and primary designation |
Deal Management
| Action | Class | Description |
|--------|-------|-------------|
| Create Deal | CreateDealAction | Creates a new deal/opportunity |
| Update Deal | UpdateDealAction | Updates deal properties and pipeline stage |
| Get Deal | GetDealAction | Retrieves deal details with associations, timeline, and metrics |
| Search Deals | SearchDealsAction | Searches deals with flexible filter criteria |
| Get Deals by Contact | GetDealsByContactAction | Retrieves all deals associated with a contact |
| Get Deals by Company | GetDealsByCompanyAction | Retrieves all deals associated with a company |
Activity Management
| Action | Class | Description |
|--------|-------|-------------|
| Log Activity | LogActivityAction | Logs calls, emails, meetings, or notes with contact/company/deal associations |
| Create Task | CreateTaskAction | Creates a task with subject, priority, due date, and owner |
| Update Task | UpdateTaskAction | Updates an existing task's properties |
| Get Activities by Contact | GetActivitiesByContactAction | Retrieves all activities for a specific contact |
| Get Upcoming Tasks | GetUpcomingTasksAction | Gets upcoming/overdue tasks filtered by owner, status, and priority |
Usage Examples
Creating a Contact
import { ActionEngineServer } from '@memberjunction/actions';
const engine = ActionEngineServer.Instance;
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'CreateContactAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'Email', Type: 'Input', Value: '[email protected]' },
{ Name: 'FirstName', Type: 'Input', Value: 'Jane' },
{ Name: 'LastName', Type: 'Input', Value: 'Doe' },
{ Name: 'Company', Type: 'Input', Value: 'Acme Corp' },
{ Name: 'JobTitle', Type: 'Input', Value: 'VP of Sales' },
{ Name: 'LifecycleStage', Type: 'Input', Value: 'lead' },
{ Name: 'AssociateWithCompanyId', Type: 'Input', Value: 'hubspot-company-id' }
],
ContextUser: contextUser
});
if (result.Success) {
const summary = result.Params.find(p => p.Name === 'Summary')?.Value;
console.log(`Created contact ${summary.contactId} at ${summary.portalUrl}`);
}Searching Contacts
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'SearchContactsAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'LifecycleStage', Type: 'Input', Value: 'lead' },
{ Name: 'CreatedAfter', Type: 'Input', Value: '2025-01-01' },
{ Name: 'SortBy', Type: 'Input', Value: 'createdate' },
{ Name: 'SortOrder', Type: 'Input', Value: 'DESC' },
{ Name: 'Limit', Type: 'Input', Value: 50 }
],
ContextUser: contextUser
});
if (result.Success) {
const contacts = result.Params.find(p => p.Name === 'Contacts')?.Value;
const summary = result.Params.find(p => p.Name === 'Summary')?.Value;
console.log(`Found ${summary.totalResults} contacts`);
console.log('By lifecycle stage:', summary.resultStats.byLifecycleStage);
}Logging an Activity
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'LogActivityAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'ActivityType', Type: 'Input', Value: 'CALL' },
{ Name: 'Subject', Type: 'Input', Value: 'Follow-up call' },
{ Name: 'Body', Type: 'Input', Value: 'Discussed renewal terms' },
{ Name: 'Status', Type: 'Input', Value: 'COMPLETED' },
{ Name: 'DurationMilliseconds', Type: 'Input', Value: 900000 },
{ Name: 'ContactIds', Type: 'Input', Value: ['contact-id-1'] },
{ Name: 'DealIds', Type: 'Input', Value: ['deal-id-1'] }
],
ContextUser: contextUser
});Retrieving a Deal with Associations
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'GetDealAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'DealId', Type: 'Input', Value: 'hubspot-deal-id' },
{ Name: 'IncludeAssociations', Type: 'Input', Value: true },
{ Name: 'IncludeTimeline', Type: 'Input', Value: true }
],
ContextUser: contextUser
});
if (result.Success) {
const summary = result.Params.find(p => p.Name === 'Summary')?.Value;
console.log(`Deal: ${summary.dealName}, Stage: ${summary.dealStage}`);
console.log(`Amount: ${summary.amount}, Days open: ${summary.metrics.daysOpen}`);
console.log(`Overdue: ${summary.metrics.isOverdue}`);
}Getting Upcoming Tasks
const result = await engine.RunAction({
Action: engine.Actions.find(a => a.Name === 'GetUpcomingTasksAction')!,
Params: [
{ Name: 'CompanyID', Type: 'Input', Value: 'company-uuid' },
{ Name: 'OwnerId', Type: 'Input', Value: 'owner-id' },
{ Name: 'DaysAhead', Type: 'Input', Value: 14 },
{ Name: 'IncludeOverdue', Type: 'Input', Value: true },
{ Name: 'Statuses', Type: 'Input', Value: ['NOT_STARTED', 'IN_PROGRESS'] }
],
ContextUser: contextUser
});
if (result.Success) {
const summary = result.Params.find(p => p.Name === 'Summary')?.Value;
console.log(`Total tasks: ${summary.totalTasks}`);
console.log(`Overdue: ${summary.overdueCount}, Due today: ${summary.dueTodayCount}`);
console.log('By due date:', summary.byDueDate);
}API Reference
Base Classes
BaseCRMAction
Abstract base class for all CRM actions. Provides:
| Method | Description |
|--------|-------------|
| getCompanyIntegration(companyId, contextUser) | Looks up and caches the CompanyIntegration entity for a given company |
| getCredentialFromEnv(companyId, credentialType) | Resolves credentials from environment variables using BIZAPPS_{PROVIDER}_{ID}_{TYPE} format |
| getAPICredentials(integration) | Resolves API credentials with env-var-first, database-fallback strategy |
| getAPIBaseURL(integration) | Returns the base URL for API calls (from CustomAttribute1 or provider default) |
| formatCRMDate(date) | Formats a Date to ISO 8601 string |
| parseCRMDate(dateString) | Parses an ISO 8601 string to Date |
| formatPhoneNumber(phone) | Normalizes phone numbers toward E.164 format |
| isValidEmail(email) | Validates email format with a regex check |
| mapDealStatus(stage) | Maps CRM-specific deal stage names to 'open' \| 'won' \| 'lost' \| 'unknown' |
| mapActivityType(type) | Maps activity type strings to 'call' \| 'email' \| 'meeting' \| 'task' \| 'note' \| 'other' |
HubSpotBaseAction
Provider-specific base class extending BaseCRMAction. Adds HubSpot v3 API support:
| Method | Description |
|--------|-------------|
| makeHubSpotRequest<T>(endpoint, method, body, contextUser) | Makes an authenticated request to the HubSpot CRM v3 API |
| makeHubSpotPaginatedRequest<T>(endpoint, params, contextUser) | Handles cursor-based pagination with configurable MaxResults limit |
| searchHubSpotObjects<T>(objectType, filters, properties, contextUser) | Executes a HubSpot search API call with filter groups |
| batchCreateHubSpotObjects<T>(objectType, objects, contextUser) | Batch-creates objects in HubSpot |
| batchUpdateHubSpotObjects<T>(objectType, updates, contextUser) | Batch-updates objects in HubSpot |
| associateObjects(fromType, fromId, toType, toId, assocType, contextUser) | Creates an association between two HubSpot objects |
| getAssociationTypeId(fromType, toType) | Returns the default HubSpot association type ID for a given pair |
| mapHubSpotProperties(hubspotObject) | Flattens a HubSpot object's properties map alongside id, createdAt, updatedAt, archived |
Common Parameters
Every action accepts at minimum:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| CompanyID | Input | Yes | The MemberJunction CompanyIntegration company identifier |
Most actions also produce:
| Parameter | Type | Description |
|-----------|------|-------------|
| Summary | Output | Structured summary of the operation result |
Result Codes
Actions return standardized result codes:
| Code | Meaning |
|------|---------|
| SUCCESS | Operation completed successfully |
| PARTIAL_SUCCESS | Batch operation with some failures (e.g., associating multiple contacts) |
| VALIDATION_ERROR | Missing or invalid input parameters |
| DUPLICATE_CONTACT | Contact with the given email already exists |
| DEAL_NOT_FOUND | Requested deal ID does not exist |
| NOT_FOUND | Requested resource does not exist |
| ERROR | General operation failure |
Source Structure
src/
index.ts # Package entry point
base/
base-crm.action.ts # BaseCRMAction - domain base class
providers/
hubspot/
index.ts # HubSpot provider exports
hubspot-base.action.ts # HubSpotBaseAction - provider base class
actions/
index.ts # Action barrel exports
create-contact.action.ts # Contact CRUD
update-contact.action.ts
get-contact.action.ts
search-contacts.action.ts
delete-contact.action.ts
merge-contacts.action.ts
create-company.action.ts # Company management
update-company.action.ts
get-company.action.ts
search-companies.action.ts
associate-contact-to-company.action.ts
create-deal.action.ts # Deal management
update-deal.action.ts
get-deal.action.ts
search-deals.action.ts
get-deals-by-contact.action.ts
get-deals-by-company.action.ts
log-activity.action.ts # Activity management
create-task.action.ts
update-task.action.ts
get-activities-by-contact.action.ts
get-upcoming-tasks.action.tsDependencies
| Package | Purpose |
|---------|---------|
| @memberjunction/actions | BaseAction class and ActionEngineServer |
| @memberjunction/actions-base | ActionParam, RunActionParams, ActionResultSimple types |
| @memberjunction/core | UserInfo, Metadata, RunView |
| @memberjunction/core-entities | CompanyIntegrationEntity for credential lookup |
| @memberjunction/global | @RegisterClass decorator for class factory registration |
Related Packages
- @memberjunction/actions-bizapps-accounting - Accounting system integrations (QuickBooks, etc.)
- @memberjunction/actions-bizapps-lms - Learning management system integrations (LearnWorlds, etc.)
- @memberjunction/actions - Actions execution engine
- @memberjunction/actions-base - Base classes and type definitions
Adding New Providers
To add a new CRM provider (e.g., Salesforce):
- Create a provider directory:
src/providers/salesforce/ - Create a provider base class extending
BaseCRMAction:- Set
crmProviderandintegrationName - Implement provider-specific authentication and request methods
- Set
- Add individual action classes in
src/providers/salesforce/actions/ - Register each action with
@RegisterClass(BaseAction, 'ActionName') - Export from provider
index.tsand the package rootindex.ts - Configure Integration and CompanyIntegration records for the new provider
