Skip to content

@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.

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:#fff

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:#fff
Terminal window
npm install @memberjunction/actions-bizapps-crm

This package is server-side only. It is typically consumed by MJAPI or other server-side hosts that register actions via the MemberJunction class factory.

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');

Create a CompanyIntegration record for each tenant/company that uses HubSpot:

INSERT INTO CompanyIntegration (CompanyID, IntegrationID, ExternalSystemID, IsActive)
VALUES (@CompanyID, @HubSpotIntegrationID, @HubSpotPortalID, 1);

Credentials are resolved from environment variables first, then from database fields.

Environment variables (recommended):

Terminal window
# 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_secret

Database fallback: store credentials in the AccessToken, APIKey, or RefreshToken columns of the CompanyIntegration entity.

ActionClassDescription
Create ContactCreateContactActionCreates a new contact with email validation and optional company association
Update ContactUpdateContactActionUpdates an existing contact’s properties
Get ContactGetContactActionRetrieves a single contact by ID with optional associations
Search ContactsSearchContactsActionSearches contacts with filters, sorting, pagination, and date ranges
Delete ContactDeleteContactActionDeletes (archives) a contact
Merge ContactsMergeContactsActionMerges duplicate contacts into a single record
ActionClassDescription
Create CompanyCreateCompanyActionCreates a new company record
Update CompanyUpdateCompanyActionUpdates an existing company’s properties
Get CompanyGetCompanyActionRetrieves a single company by ID
Search CompaniesSearchCompaniesActionSearches companies with flexible filter criteria
Associate Contact to CompanyAssociateContactToCompanyActionLinks contacts to companies with batch support and primary designation
ActionClassDescription
Create DealCreateDealActionCreates a new deal/opportunity
Update DealUpdateDealActionUpdates deal properties and pipeline stage
Get DealGetDealActionRetrieves deal details with associations, timeline, and metrics
Search DealsSearchDealsActionSearches deals with flexible filter criteria
Get Deals by ContactGetDealsByContactActionRetrieves all deals associated with a contact
Get Deals by CompanyGetDealsByCompanyActionRetrieves all deals associated with a company
ActionClassDescription
Log ActivityLogActivityActionLogs calls, emails, meetings, or notes with contact/company/deal associations
Create TaskCreateTaskActionCreates a task with subject, priority, due date, and owner
Update TaskUpdateTaskActionUpdates an existing task’s properties
Get Activities by ContactGetActivitiesByContactActionRetrieves all activities for a specific contact
Get Upcoming TasksGetUpcomingTasksActionGets upcoming/overdue tasks filtered by owner, status, and priority
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: 'jane.doe@example.com' },
{ 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}`);
}
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);
}
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
});
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}`);
}
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);
}

Abstract base class for all CRM actions. Provides:

MethodDescription
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'

Provider-specific base class extending BaseCRMAction. Adds HubSpot v3 API support:

MethodDescription
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

Every action accepts at minimum:

ParameterTypeRequiredDescription
CompanyIDInputYesThe MemberJunction CompanyIntegration company identifier

Most actions also produce:

ParameterTypeDescription
SummaryOutputStructured summary of the operation result

Actions return standardized result codes:

CodeMeaning
SUCCESSOperation completed successfully
PARTIAL_SUCCESSBatch operation with some failures (e.g., associating multiple contacts)
VALIDATION_ERRORMissing or invalid input parameters
DUPLICATE_CONTACTContact with the given email already exists
DEAL_NOT_FOUNDRequested deal ID does not exist
NOT_FOUNDRequested resource does not exist
ERRORGeneral operation failure
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.ts
PackagePurpose
@memberjunction/actionsBaseAction class and ActionEngineServer
@memberjunction/actions-baseActionParam, RunActionParams, ActionResultSimple types
@memberjunction/coreUserInfo, Metadata, RunView
@memberjunction/core-entitiesCompanyIntegrationEntity for credential lookup
@memberjunction/global@RegisterClass decorator for class factory registration

To add a new CRM provider (e.g., Salesforce):

  1. Create a provider directory: src/providers/salesforce/
  2. Create a provider base class extending BaseCRMAction:
    • Set crmProvider and integrationName
    • Implement provider-specific authentication and request methods
  3. Add individual action classes in src/providers/salesforce/actions/
  4. Register each action with @RegisterClass(BaseAction, 'ActionName')
  5. Export from provider index.ts and the package root index.ts
  6. Configure Integration and CompanyIntegration records for the new provider