You can call the model form of .populate() on the result objects from an aggregate operation. But the thing is you are going to need a model to represent the "Result" object returned by your aggregation in order to do so.
There are a couple of steps, best explained with a complete listing:
var async = require('async'), mongoose = require('mongoose'), Schema = mongoose.Schema; var employeeSchema = new Schema({ "fname": String, "lname": String }) var availSchema = new Schema({ "is_afternoon_scheduled": Boolean, "employee_id": { "type": Schema.Types.ObjectId, "ref": "Employee" } }); var resultSchema = new Schema({ "_id": { "type": Schema.Types.ObjectId, "ref": "Employee" }, "count": Number }); var Employee = mongoose.model( "Employee", employeeSchema ); var Availability = mongoose.model( "Availability", availSchema ); var Result = mongoose.model( "Result", resultSchema, null ); mongoose.connect('mongodb://localhost/aggtest'); async.series( [ function(callback) { async.each([Employee,Availability],function(model,callback) { model.remove({},function(err,count) { console.log( count ); callback(err); }); },callback); }, function(callback) { async.waterfall( [ function(callback) { var employee = new Employee({ "fname": "abc", "lname": "xyz" }); employee.save(function(err,employee) { console.log(employee), callback(err,employee); }); }, function(employee,callback) { var avail = new Availability({ "is_afternoon_scheduled": true, "employee_id": employee }); avail.save(function(err,avail) { console.log(avail); callback(err); }); } ], callback ); }, function(callback) { Availability.aggregate( [ { "$group": { "_id": "$employee_id", "count": { "$sum": 1 } }} ], function(err,results) { results = results.map(function(result) { return new Result( result ); }); Employee.populate(results,{ "path": "_id" },function(err,results) { console.log(results); callback(err); }); } ); } ], function(err,result) { if (err) throw err; mongoose.disconnect(); } );
That's the complete example, but taking a closer look at what happens inside the aggregate result is the main point:
function(err,results) { results = results.map(function(result) { return new Result( result ); }); Employee.populate(results,{ "path": "_id" },function(err,results) { console.log(results); callback(err); }); }
The first thing to be aware of is that the results returned by .aggregate() are not mongoose documents as they would be in a .find() query. This is because aggregation pipelines typically alter the document in results from what the original schema looked like. Since it is just a raw object, each element is re-cast as a mongoose document for the Result model type defined earlier.
Now in order to .populate() with data from Employee, the model form of this method is called on the array of results in document object form along with the "path" argument to the field to be populated.
The end result fills is the data as it comes from the Employee model it was related to.
[ { _id: { _id: 54ab2e3328f21063640cf446, fname: 'abc', lname: 'xyz', __v: 0 }, count: 1 } ]
Different to how you process with find, but it is necessary to "re-cast" and manually call in this way due to how the results are returned.