Airtable: Creating a CRM (Part 4) – Convert the Web Lead

In our previous posts in this series, we started our base with a data model of Organizations, Contacts, and Interactions, then followed that up by adding a Web Leads table and a web form for capturing those leads on our website, and then we created an Opportunities table to track our pipeline, wins & losses. In this post, we are going to build a process for taking the web lead data and bringing it into the main data model.

The concept of having Leads in a separate table and then “converting” those leads to other tables (one qualified) is one that comes from my time using Salesforce.

New / Changed Fields

Web Leads

To accommodate this script, we are going to add some fields to the Web Leads table.

Field Name Data Type Description
Converted Organization Link to Organizations Update this field to it only allows for 1 record to be linked.
Converted Contact Link to Contacts Update this field to it only allows for 1 record to be linked.
Converted Opportunity Link to Opportunities Update this field to it only allows for 1 record to be linked.
Converted Date Date with Time enabled  
Convert Lead Button See below for how this field should be configured.
Interactions Link to Interactions Keep this field as allowing multiple linked records

Other Changes

  • Add a value of “Web Lead” as an Interaction Type

Button Field Configuration

To invoke the process, we are going to add a button field to the Web Leads table. By choosing the Run script action, Airtable will want us to have an instance of the Scripting app in the system. Apps are only available on the Pro plan, so you will need that (or a trial of it) in order to complete the work in this post. I named my Scripting App “Convert Web Lead”

Script Flow

The Convert Lead button field is on the Web Lead record now. Clicking that button will invoke the script in the scripting app. As you can see, I created 2 new views: Open Leads and Converted Leads. When the script is successful, it will mark the Web Lead with a status of Converted. This will cause that lead to fall off the Open Leads view.

1) Match a Contact

When the script runs, it is going to determine whether or not we already have a contact in the system for this lead. We don’t want to create a duplicate. It will do this by:

  • Matching the lead email with all contact email fields
  • Match the lead name with an existing contact name

If it finds some Contacts, it will present the user with what you see below. The user will use the picker to pick a record.

If it cannot find a match, the script just continues without asking the user, assuming it will be creating a new Contact record.

2) Match the Organization

If the user matched a Contact, the system will note that Contact’s organization. The system will also:

  • Match the lead organization name with an existing organization name

If it finds some Organizations, it will present the user with what you see below. The user will use the picker to pick a record.

If it cannot find a match, the script just continues without asking the user, assuming it will be creating a new Organization record.

3) Ask about Opportunity Creation

The user is presented with a prompt whether they’d like an Opportunity created?

4) Ask about Interaction Creation

The user is presented with a prompt whether they’d like an Interaction created? It would be recommended to create one.

5) Confirm actions to be taken

Before the script actually interacts with the system’s data, it will confirm what will happen next.

The Results

The Web Lead was updated with a status of Converted and with links to all of the records involved in the process.

In this example, the script was able to find the Organization and Contact in the system, so no new records were created, but it did create a new Opportunity and Interaction record.

The Opportunity represents the potential sale.

The Interaction documents that the web lead actually took place and is viewable from all records involved.

In this CRM series, we have not yet gotten to To Dos (i.e. Tasks). Once we do, I’d want to update this script to have a “Respond to Web Lead” To Do assigned to the user converting the lead.

The Script

This script should work as long as you created all of the fields in your base as I have them labelled in this blog post series.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
// Define primary table and select a record from it
let table = base.getTable( "Web Leads" );
let record = await input.recordAsync( 'Select a record to convert...' , table); // A button field will automatically bypass this UI step and select the record where the button was clicked
 
// Get other tables into memory
let conTable = base.getTable( "Contacts" );
let orgTable = base.getTable( "Organizations" );
let oppTable = base.getTable( "Opportunities" );
let intTable = base.getTable( "Interactions" );
 
 
if (!record) { output.markdown( '**A record was not selected. Please try again.**' ); // handle exception - no record selected
} else { // a record was selected
     if (record.getCellValueAsString( "Status" ) == "Converted" ){ output.markdown(`**This record has already been convertred and cannot be converted again**`); // handle exception - already converted
     } else { // MAIN PROCESSING CODE AFTER PASSING A COUPLE CHECKS
         
         // Pull values from the lead record to use for comparing to other records
         let leadName = record.name;
         let leadEmail = record.getCellValueAsString( "Email" );
         let leadOrg = record.getCellValueAsString( "Organization Name" );
         let leadPhone = record.getCellValueAsString( "Phone" );
         let leadNotes = record.getCellValueAsString( "Notes" );
         let leadCreated = record.getCellValue( "Created" ); // important to not get the value as a string. Use getCellValue() instead
         
         // Arrays to hold matches
         let potentialOrgMatches = [];
         let potentialConMatches = [];
         let orgIds = []; // list of organization ids from the matched contacts. We use this to help determine the list of Organizations
 
         // QUERY CONTACTS AND FIND POTENTIAL MATCHES
         let conQuery = await conTable.selectRecordsAsync({
             fields: [ "Name" , "Email" , "Alt Email" , "Organizations" ]
         });
 
         // Loop through Contacts
         for ( let theContact of conQuery.records) {
             
             // Get Contact field values
             let conEmail = theContact.getCellValueAsString( "Email" );
             let contAltEmail = theContact.getCellValueAsString( "Alt Email" );
             let conOrgs = theContact.getCellValue( "Organizations" );
 
             // Compare contact email with lead email
             if ( conEmail != "" ){
                 if (conEmail == leadEmail){
                     potentialConMatches.push(theContact); // add contact as a potential match
                     if (conOrgs != null ){
                         for ( let theConOrg of conOrgs){
                             orgIds.push(theConOrg.id); // add contact's org as a potential org to link to
                         }
                     }
                 }
             }
 
             // Compare contact alt email with lead email
             if (contAltEmail != "" ){
                 if (contAltEmail == leadEmail){
                     potentialConMatches.push(theContact);  // add contact as a potential match
                     for ( let theConOrg of conOrgs){
                         orgIds.push(theConOrg.id); // add contact's org as a potential org to link to
                     }
                 }
             }
 
             // Compare Contact name with Lead name
             if (theContact.name != "" ){
                 if (theContact.name == leadName){
                     potentialConMatches.push(theContact);  // add contact as a potential match
                     for ( let theConOrg of conOrgs){
                         orgIds.push(theConOrg.id); // add contact's org as a potential org to link to
                     }
                 }
             }
             
         }
 
         // QUERY ORGANIZATIONS AND FIND POTENTIAL MATCHES
         let orgQuery = await orgTable.selectRecordsAsync({
             fields: [ "Name" ]
         })
 
         // Loop through Organizations
         for ( let theOrg of orgQuery.records) {
 
             // Compare name values with each other
             if (theOrg.name == leadOrg){
                 potentialOrgMatches.push(theOrg); // add org as a potential match
             }
 
             // Compare Id values with the Org IDs from the matching Contacts
             if (orgIds.includes(theOrg.id)){
                 potentialOrgMatches.push(theOrg); // add org as a potential match
             }
         }
 
 
         // HANDLE MATCH SCENARIOS AND BUILD UI PROCESS ACCORDINGLY
         let selectedOrg = null ; // assume no record is selected and we create a new organization
         let selectedCon = null ; // assume no record is selected and we create a new contact
 
         // Proceed through the potential Contact matches
         if (potentialConMatches.length > 0){
             
             // Have user select a Contact. If they don't select one, the selectedCon will stay null
             selectedCon = await input.recordAsync(`Potential Contact matches were found. Use the picker below to choose a matching Contact If you would like to create a new Contact, please open the picker and then cancel out of it.`, potentialConMatches);
             
             // Redetermine the potential orgs if a contact was selected
             if (selectedCon != null ){
                 
                 // Get the Org IDs for this Contact into an array
                 orgIds = []; // clear orgIds array
                 if (selectedCon.getCellValue( "Organizations" ) != null ){
                     for ( let theConOrg of selectedCon.getCellValue( "Organizations" )){
                         orgIds.push(theConOrg.id);
                     }
                 }
 
                 // Pare down potentialOrgMatches[] to only the orgs from the selected Contact
                 let selectedConOrgMatches = [];
                 for ( let theOrg of potentialOrgMatches){
                     if (orgIds.includes(theOrg.id)){
                         selectedConOrgMatches.push(theOrg);
                     }
                 }
                 if (selectedConOrgMatches.length == 1){ // Selected contact only has 1 org
                     selectedOrg = selectedConOrgMatches[0];
                 } else if (selectedConOrgMatches.length > 1){ // Selected Contact has many Organizations
                     potentialOrgMatches = []; // clear the main matches array
                     potentialOrgMatches = selectedConOrgMatches;
                 }
             }
 
         }
 
         
         if (
             (selectedCon == null && potentialOrgMatches.length > 0) ||  // No Contact selected and there are potential Org matches to choose from
             (selectedCon != null && selectedOrg == null && potentialOrgMatches.length > 0) // A Contact was selected, but an org hasn't been selected yet
         ){
             selectedOrg = await input.recordAsync(`Potential Organization matches were found. Use the picker below to either choose a matching Organization or cancel out of the picker to indicate a new Organization should be created.`, potentialOrgMatches);
         }
 
         // Ask if we need to create an interaction and/or opportunity
         let bOpportunity = await input.buttonsAsync('Would you like a new Opportunity record to be created? ', [' Yes ', ' No ']);
         let bInteraction = await input.buttonsAsync(' Would you like a new Interaction record to be created to document the Web Lead that took place? ', [' Yes ', ' No ']);
 
         // Final IDs
         let finalOrg = null;
         let finalOrgName = ' ';
         let finalCon = null;
         let finalOpp = null;
         let finalInt = null;
         
         // Based upon selections, confirm that the user would like to proceed
         output.markdown(`**Based upon this information, the following will happen next:**`);
         if(selectedOrg == null){
             output.markdown(`* A new Organization named **` + leadOrg + `** will be created.`);
         }
         if(selectedCon == null){
             output.markdown(`* A new Contact named **` + leadName + `** will be created.`);
             if(selectedOrg == null){
                 output.markdown(`* This new Contact will be linked to the new **` + leadOrg + `** Organization being created.`);
             } else {
                 output.markdown(`* This new Contact will be linked to the existing **` + selectedOrg.name + `** Organization record.`);
                 finalOrg = selectedOrg.id;
                 finalOrgName = selectedOrg.name;
             }
         } else {
             output.markdown(`* The existing Contact **` + selectedCon.name + `** will be used. A new Contact will **not** be created.`);
             finalCon = selectedCon.id;
             if(selectedOrg != null){
                 output.markdown(`* The existing Organization **` + selectedOrg.name + `** will be used. A new Organization will **not** be created.`);
                 finalOrg = selectedOrg.id;
                 finalOrgName = selectedOrg.name;
             }
         }
 
         output.markdown(`* The ` + leadName + ` Web Lead will be marked as **Converted** and linked to the Organization and Contact.`);
 
         if(bOpportunity == "Yes"){
             output.markdown(`* A new Opportunity will be created.`);
         } else if (bOpportunity == "No"){
             output.markdown(`* A new Opportunity will **not** be created.`);
         }
 
         if(bInteraction == "Yes"){
             output.markdown(`* A new Interaction will be created.`);
         } else if (bInteraction == "No"){
             output.markdown(`* A new Interaction will **not** be created.`);
         }
 
         let bContinue = await input.buttonsAsync(' Would you like to continue converting this lead? If you choose Yes, your system\ 's data will be affected.' , [ 'Yes' , 'No' ]);
         if (bContinue == "Yes" ){
             
             // CREATE & MODIFY DATA AS NEEDED
 
             // Create Org
             if (finalOrg == null ){
                 finalOrg = await orgTable.createRecordAsync({
                     "Name" : leadOrg
                 });
                 finalOrgName = leadOrg;
                 output.markdown(`The new account can be found [here](https: //airtable.com/` + base.id + `/` + orgTable.id + `/` + finalOrg + `)`);
             }
             
             // Create Contact
             if (finalCon == null ){
                 finalCon = await conTable.createRecordAsync({
                     "Organizations" : [{id: finalOrg}],
                     "Name" : leadName,
                     "Email" : leadEmail,
                     "Work Phone" : leadPhone
                 });
                 output.markdown(`The new contact can be found [here](https: //airtable.com/` + base.id + `/` + conTable.id + `/` + finalCon + `)`);
             }
 
             if (bOpportunity == "Yes" ){
                 let currentDate = new Date();
                 finalOpp = await oppTable.createRecordAsync({
                     "Organization" : [{id: finalOrg}],
                     "Related Contacts" : [{id: finalCon}],
                     "Owner" : {id: session.currentUser.id},
                     "Name" : finalOrgName + "- Opportunity from Web Lead" ,
                     "Type" : {name: "New" },
                     "Stage" : {name: "Prospect" },
                     "Lead Source" : {name: "Web Lead" },
                     "Close Date" : new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 30)
                 });
                 output.markdown(`The new opportunity can be found [here](https: //airtable.com/` + base.id + `/` + oppTable.id + `/` + finalOpp + `)`);
             }
             
             if (bInteraction == "Yes" ){
                 let finalIntObject = {
                     "Organizations" : [{id: finalOrg}],
                     "Contacts" : [{id: finalCon}],
                     "Web Leads" : [{id: record.id}],
                     "Type" : {name: "Web Lead" },
                     "Subject" : "Web Lead Submitted" ,
                     "Notes" : leadNotes,
                     "Date & Time" : leadCreated
                 };
                 if (bOpportunity == "Yes" && finalOpp != null ) {
                     finalIntObject[ "Opportunities" ] = [{id: finalOpp}];
                 }
                 finalInt = await intTable.createRecordAsync(finalIntObject);
                 output.markdown(`The new interaction record can be found [here](https: //airtable.com/` + base.id + `/` + intTable.id + `/` + finalInt + `)`);
             }
 
             let updateObject = {};
             updateObject[ "Status" ] = {name: "Converted" };
             updateObject[ "Converted Organization" ] = [{id: finalOrg}];
             updateObject[ "Converted Contact" ] = [{id: finalCon}];
             updateObject[ "Converted Date" ] = new Date();
             if (bOpportunity == "Yes" ){
                 updateObject[ "Converted Opportunity" ] = [{id: finalOpp}];
             }
             table.updateRecordAsync(record.id, updateObject);
 
 
         } else if (bContinue == "No" ){
             output.markdown(`**The operation was cancelled. No system changes were made.**`);
         }
     }
     
}

 

Scott Hemmeter

Comments

Related posts

Search Get Crypto Prices with Airtable Scripts
Quick Method to Setup Single Select & Multi Select Field Value Lists Search