function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Amanda ReamAmanda Ream 

Trigger a Chatter Post when an Opportunity is won

Hello Everyone,
I am an APEX newbie trying to create a trigger that posts when an Opp is won. I have no other requirements besides that the when an Opportunity is updated to the won stage, it posts a pedesigned message to a specific Chatter Group. I took the code from another post (https://developer.salesforce.com/forums/ForumsMain?id=906F00000009AFJIA2) and tried to modify it to do just that but I am running into a problem in the IDE at the "for" line, with the error stating "Save error: Loop must iterate over collection type: Boolen". I tried to add an else statement to break and exit the loop but that gave me a bracket error. Am I totally off base here? Should I pulling a list first for the function to iterate over? Any help would be much appreciated! Thanks!

trigger ChatterPostWonOpp on Opportunity (after update) {

String status;
String OppAccName;
String OppOwnerName;

FeedItem post = new FeedItem();
  
    for(Opportunity o : Trigger.isupdate) {
                if(o.IsWon == true ) //This will be executed on new record insertion when Opp is won
                    for (Opportunity oppty : [SELECT Account.Name, Owner.Name FROM Opportunity])
                     {
                        OppAccName = oppty.Account.Name;
                        OppOwnerName = oppty.Owner.Name;
                      }  
                
                    status = OppOwnerName + ' just won ' + OppAccName + ' for ' + o.Amount + '!';

                    post.ParentId = '0F9g00000008b7c';
                    post.Title = o.Name;
                    post.Body = status;
                  
                    insert post;
          }
    }
Best Answer chosen by Amanda Ream
CheyneCheyne
Trigger.isupdate is a boolean variable which simply tells you whether or not the trigger is running as a result of a record being updated. Since this trigger only runs on updates, this is always true. You need to iterate over a list of opportunities. If you were only using opportunity fields, you could use Trigger.new, which is a list of all of the updated opportunities in the current context. Since you need related fields from the account and owner, then you need to query for a list of opportunities. It's important that you don't do this query inside of the for loop, since you could run into governor limits that way. You also should not run the insert statement inside the for loop for the same reason. To run your query, you can use Trigger.newMap.keySet(), which will give you a list of the IDs of all of the opportunities that were updated in the current trigger context.

You also should not hardcode the chatter group ID, since this value will change when you deploy your code to production. You can use a query to get that information as well.

Putting all of this together, your code might look something like this:

trigger ChatterPostWonOpp on Opportunity (after update) {
    List<FeedItem> posts = new List<FeedItem>();
    List<Opportunity> updatedOpps = [SELECT Name, Account.Name, Owner.Name, Amount FROM Opportunity WHERE Id IN :Trigger.newMap.keySet() AND IsWon = true];
    CollaborationGroup chatterGroup = [SELECT Id FROM CollaborationGroup WHERE Name = 'ChatterGroupName' LIMIT 1];
    for (Opportunity opp : updatedOpps) {
        String status = opp.Owner.Name + ' just won ' + opp.Account.Name + ' for ' + opp.Amount + '!';
        FeedItem post = new FeedItem(
            ParentId = chatterGroup.Id,
            Title = opp.Name,
            Body = status
        );
        posts.add(post);
    }
    insert posts;
}

All Answers

CheyneCheyne
Trigger.isupdate is a boolean variable which simply tells you whether or not the trigger is running as a result of a record being updated. Since this trigger only runs on updates, this is always true. You need to iterate over a list of opportunities. If you were only using opportunity fields, you could use Trigger.new, which is a list of all of the updated opportunities in the current context. Since you need related fields from the account and owner, then you need to query for a list of opportunities. It's important that you don't do this query inside of the for loop, since you could run into governor limits that way. You also should not run the insert statement inside the for loop for the same reason. To run your query, you can use Trigger.newMap.keySet(), which will give you a list of the IDs of all of the opportunities that were updated in the current trigger context.

You also should not hardcode the chatter group ID, since this value will change when you deploy your code to production. You can use a query to get that information as well.

Putting all of this together, your code might look something like this:

trigger ChatterPostWonOpp on Opportunity (after update) {
    List<FeedItem> posts = new List<FeedItem>();
    List<Opportunity> updatedOpps = [SELECT Name, Account.Name, Owner.Name, Amount FROM Opportunity WHERE Id IN :Trigger.newMap.keySet() AND IsWon = true];
    CollaborationGroup chatterGroup = [SELECT Id FROM CollaborationGroup WHERE Name = 'ChatterGroupName' LIMIT 1];
    for (Opportunity opp : updatedOpps) {
        String status = opp.Owner.Name + ' just won ' + opp.Account.Name + ' for ' + opp.Amount + '!';
        FeedItem post = new FeedItem(
            ParentId = chatterGroup.Id,
            Title = opp.Name,
            Body = status
        );
        posts.add(post);
    }
    insert posts;
}
This was selected as the best answer
Sushil KaushikSushil Kaushik
Hi Amanda,

seems to me a problem at line number 8 of your trigger code i.e.  for(Opportunity o : Trigger.isupdate)  
 Trigger.isupdate retuns a boolean value and you can't iterate over a boolean value.  Use 'Trigger.old' or 'Trigger.new' instead.

You can use any one of Trigger.old or Trigger.new depending on your logic.  Trigger.old contains the object prior to the updation whereas new have updated object. In my view, in your case trigger.new will  do the trick. but i am not sure, so, try yourself which one is working for you.

Regards,

Sushil.
Amanda ReamAmanda Ream
Hi Cheyne, 
Thank so much for the help and quick response! I am going to work on this in the afternoon (EST) and I will let you know how I make out. 
Amanda ReamAmanda Ream
Hi Sushil,
Thank you for your input! I think I will need the Trigger.new because I want the iteration to happen after the status has been changed. ! am working on it now. I will let you know what I find. Thanks!
Amanda ReamAmanda Ream
Hi Sushil and Cheyne,

I want to thank you both again for your help.

I took both of your suggestions and tried to make them work. Testing both helped me understand the two different approaches and will help me in the long run learn more about APEX. I think I will probably end up using Cheyne's approach because it respects the governor limits and this is something I was warned about in ADM231. However, when I tried Cheyne's approach, I got a "String Error". This is my fault because I didn't change one of the fields in the copied formula. What I posted above is checking the IsWon field which I assume is a boolean field in the user's Org who posted this formula to the forum. The field I want to check is called Stage Name and is a picklist field. Also, rather than checking whether it is true or false, I want to know if it contains the word 'Won'. I found the contains function and replaced it, I think correctly, in the code but now I am getting a "Unexpected token error" on the word 'Won'. I am thinking this maybe has something to do with it being a String and I need to somehow convert the value to a String? I did some searching and found that you can use something like the following to convert it.

contact c = new contact();
.....
.....
String str = c.multiPickList__c;
if (str.contains('TEXTVALUE') {}
.......

This would work I think in the original formula but not in the one suggested by Cheyne because there is no more 'if' statement so I am stuck as to where I should go from here. Any ideas?

trigger ChatterPostWonOpp2 on Opportunity (after update) {

    List<FeedItem> posts = new List<FeedItem>();
    List<Opportunity> updatedOpps = [SELECT Name, Account.Name, Owner.Name, Amount FROM Opportunity WHERE Id IN :Trigger.newMap.keySet() AND stagename.contains('Won')];
    CollaborationGroup chatterGroup = [SELECT Id FROM CollaborationGroup WHERE Name = 'OURCHATTERGROUNAME' LIMIT 1];
    for (Opportunity opp : updatedOpps) {
        String status = opp.Owner.Name + ' just won ' + opp.Account.Name + ' for ' + opp.Amount + '!';
        FeedItem post = new FeedItem(
            ParentId = chatterGroup.Id,
            Title = opp.Name,
            Body = status
        );
        posts.add(post);
    }
    insert posts;
}
CheyneCheyne
The problem is that you cannot use .contains() within a SOQL query. Instead, try using the LIKE operator, by writing

AND stagename LIKE '%Won%'

You can find details on the different types of operators you can use in SOQL queries at http://www.salesforce.com/us/developer/docs/dbcom_soql_sosl/Content/sforce_api_calls_soql_select_comparisonoperators.htm (http://www.salesforce.com/us/developer/docs/dbcom_soql_sosl/Content/sforce_api_calls_soql_select_comparisonoperators.htm" target="_blank)
Amanda ReamAmanda Ream
I finally got it, well you did! Thanks so much for your help!
Amanda ReamAmanda Ream
I think I spoke too soon. After doing some testing I found that this code posts to Chatter everytime the Opp is updated and the Stage contains 'Won'. The post should only happen once when it hits any 'Won' Stage but only for the first time. There are multiple Stages that contain the word 'Won' so I tried to make the trigger dependant on the value 'Closed' in the Forecast Category dependant picklist as any of the 'Won' Stages changes the Forecast Category to 'Closed'. I thought once the field was changed to 'Closed'(by selecting a 'Won' Stage for the first time), it would not fire the Trigger because the field was not (or should not) have changed values. But I found the same thing was happening, each time the Opp was edited and moved between the different 'Won' Stages (Forecast Category should have stayed in 'Closed'), the trigger still fires and posts to Chatter again. Next, I tried connecting the 'Closed' Forecast Category to a checkbox called Is Won thinking maybe a Boolean field would not cause it to refire. I created a workflow rule that updates the Is Won checkbox to True and set the evaluation to only run when the record is created and edited to meet subsequesnt criteria. By doing this, I thought I could avoid the trigger refiring because the workflow rule would not run and rechecked the box making the trigger run again. Unfortunately this didn't work either. Any ideas on how I can make a trigger run only the first time it meets the Trigger criteria?
CheyneCheyne
Yep, you can use Trigger.oldMap and Trigger.newMap. Trigger.oldMap maps each opportunity ID to the opportunity in the state that was in before it was updated, trigger.newMap maps the ids to the updated opportunties. So, you could construct your loop this way:

for (Opportunity opp : updatedOpps) {
    //This ensures that the StageName actually changed during this update
    if (trigger.oldMap.get(opp.Id).StageName != trigger.newMap.get(opp.Id).StageName) {
        //create your chatter post
    }
}
Amanda ReamAmanda Ream
So what the additional line of code is checking is: if the Stage in the old Opp list is equal to the Opp Stage in the new list, if so, it is okay to post? 


I changed the code around a bit so that it is looking at the Forecast Category Name instead of the Stage. I did this becuase there are multiple "Won" Stages but they are all attached to the "Closed" Forecat Category Name. I found though that it is still posting when I move the Opportunity in and out of different "Won" Stages. It shouldn't be posting becuase the Forecst Category Name of "Closed" should not be changing. Did I do something wrong? Here is my current code:

trigger ChatterPostWonOpp2 on Opportunity (after update) {

    List<FeedItem> posts = new List<FeedItem>();
    List<Opportunity> updatedOpps = [SELECT Name, Account.Name, Owner.Name, Amount FROM Opportunity WHERE Id IN :Trigger.newMap.keySet() AND forecastcategoryname = 'Closed' ];
    CollaborationGroup chatterGroup = [SELECT Id FROM CollaborationGroup WHERE Name = 'All Metso MAC' LIMIT 1];
    for (Opportunity opp : updatedOpps) {
      if (trigger.oldMap.get(opp.Id).ForecastCategoryName != trigger.newMap.get(opp.Id).ForecastCategoryName) {
        String status = opp.Owner.Name + ' just won ' + opp.Name + '!' + ' Nice Job!';
        FeedItem post = new FeedItem(
            ParentId = chatterGroup.Id,
            Title = opp.Name,
            Body = status
        );
        posts.add(post);
    }
    insert posts;
}

Thanks again for all your help on this. I am really learning a lot from you.
CheyneCheyne
So, the code you posted is checking if the ForecastCategoryName before the update is not equal to the ForecastCategory name after the update.If you don't believe that value should be changing, then I would try adding some debug statements before your if statement to see what those values actually are and how you are getting inside the if block. 

for (Opportunity opp : updatedOpps) {
    system.debug('Old ForecastCategoryName ==> ' + trigger.oldMap.get(opp.Id).ForecastCategoryName);
    system.debug('New ForecastCategoryName ==> ' + trigger.newMap.get(opp.Id).ForecastCategoryName);
    //This is checking if the values are different: i.e. the value was changed during this update
    if (trigger.oldMap.get(opp.Id).ForecastCategoryName != trigger.newMap.get(opp.Id).ForecastCategoryName) {
        //create your post
    }
}

Try running it like that and then check your debug log to see what values it is outputting.
Amanda ReamAmanda Ream
Hi Cheyne, 
I played around with the debugs and test coding a little but I just don't think I have thorough understanding of testing yet. I am going to work with the APEX developer guide and see if I can get this worked out in the next few weeks. I have been pulled onto a couple other projects too so I will post an update as soon as I have one. Thanks again for all your help. You rock!